[{"content":"C 语言最大内置整型 unsigned long long 约为 $1.8 \\times 10^{19}$，一旦数字超出这个范围就会溢出。高精度计算用整数数组存储大数的每一位，配合循环模拟竖式运算，从而突破位数限制。\n问题 背景 标准整型在竞赛和工程中常常不够用，典型场景包括：\n计算 $100!$（约 158 位） 大数幂运算（如 RSA 密钥生成） 需要精确结果的金融计算 核心问题 对两个任意长度的非负整数，实现加、减、乘、除四则运算，结果精确无误差。\n约束条件 数字位数最多 $10^3$ 位（可调整 MAXN 扩展） 本文只处理非负整数；负数需额外引入符号位 思路分析 核心思想 把大数的每一位逆序存入 int 数组：d[0] 存个位，d[1] 存十位，以此类推。逆序的好处是进位方向（低位 → 高位）与数组下标增长方向一致，循环写起来最自然。\n数据结构 1 2 3 4 5 6 #define MAXN 1000 typedef struct { int d[MAXN]; /* d[0] 存最低位（逆序） */ int len; /* 当前有效位数 */ } BigInt; 数字 12345 在数组中的布局：\n下标 d[0] d[1] d[2] d[3] d[4] 值 5 4 3 2 1 各运算的关键点 加法：逐位相加，用变量 carry 记录进位，循环直到最高位进位也处理完。\n减法：逐位相减，用 borrow 记录借位，要求调用前保证 $a \\geq b$。\n乘法：双重循环，a[i] * b[j] 的结果累加到结果的第 i+j 位，最后统一处理进位。中间结果用 long long 防溢出。\n除以小整数：从最高位开始，维护余数 r，每步 r = r * 10 + d[i]，商位为 r / b，余数更新为 r % b。\n代码实现 初始化与输入输出 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; #define MAXN 1000 typedef struct { int d[MAXN]; int len; } BigInt; void init(BigInt *a) { // 清零所有位，防止遗留垃圾值影响计算 memset(a-\u0026gt;d, 0, sizeof(a-\u0026gt;d)); a-\u0026gt;len = 1; } /* 从字符串读入，自动完成逆序存储 */ void fromStr(BigInt *a, const char *s) { init(a); int n = strlen(s); a-\u0026gt;len = n; for (int i = 0; i \u0026lt; n; i++) a-\u0026gt;d[i] = s[n - 1 - i] - \u0026#39;0\u0026#39;; /* 逆序：末位 → d[0] */ } /* 打印：从最高位到最低位输出 */ void print(const BigInt *a) { for (int i = a-\u0026gt;len - 1; i \u0026gt;= 0; i--) printf(\u0026#34;%d\u0026#34;, a-\u0026gt;d[i]); printf(\u0026#34;\\n\u0026#34;); } /* 比较大小，返回 1(a\u0026gt;b) / 0(a==b) / -1(a\u0026lt;b) */ int cmp(const BigInt *a, const BigInt *b) { if (a-\u0026gt;len != b-\u0026gt;len) return a-\u0026gt;len \u0026gt; b-\u0026gt;len ? 1 : -1; for (int i = a-\u0026gt;len - 1; i \u0026gt;= 0; i--) if (a-\u0026gt;d[i] != b-\u0026gt;d[i]) return a-\u0026gt;d[i] \u0026gt; b-\u0026gt;d[i] ? 1 : -1; return 0; } 加法与减法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 /* c = a + b */ void add(const BigInt *a, const BigInt *b, BigInt *c) { init(c); int carry = 0; int n = a-\u0026gt;len \u0026gt; b-\u0026gt;len ? a-\u0026gt;len : b-\u0026gt;len; for (int i = 0; i \u0026lt; n || carry; i++) { int sum = carry; if (i \u0026lt; a-\u0026gt;len) sum += a-\u0026gt;d[i]; if (i \u0026lt; b-\u0026gt;len) sum += b-\u0026gt;d[i]; c-\u0026gt;d[i] = sum % 10; carry = sum / 10; c-\u0026gt;len = i + 1; } } /* c = a - b，要求 a \u0026gt;= b */ void sub(const BigInt *a, const BigInt *b, BigInt *c) { init(c); int borrow = 0; for (int i = 0; i \u0026lt; a-\u0026gt;len; i++) { int diff = a-\u0026gt;d[i] - borrow - (i \u0026lt; b-\u0026gt;len ? b-\u0026gt;d[i] : 0); if (diff \u0026lt; 0) { diff += 10; borrow = 1; } else borrow = 0; c-\u0026gt;d[i] = diff; } c-\u0026gt;len = a-\u0026gt;len; // 去除前导零，但至少保留 1 位 while (c-\u0026gt;len \u0026gt; 1 \u0026amp;\u0026amp; c-\u0026gt;d[c-\u0026gt;len - 1] == 0) c-\u0026gt;len--; } 乘法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /* c = a * b */ void mul(const BigInt *a, const BigInt *b, BigInt *c) { init(c); c-\u0026gt;len = a-\u0026gt;len + b-\u0026gt;len; /* 结果最多 len_a + len_b 位 */ for (int i = 0; i \u0026lt; a-\u0026gt;len; i++) { int carry = 0; for (int j = 0; j \u0026lt; b-\u0026gt;len || carry; j++) { // 用 long long 防止 int 相乘溢出 long long cur = (long long)c-\u0026gt;d[i + j] + (long long)a-\u0026gt;d[i] * (j \u0026lt; b-\u0026gt;len ? b-\u0026gt;d[j] : 0) + carry; c-\u0026gt;d[i + j] = cur % 10; carry = cur / 10; } } while (c-\u0026gt;len \u0026gt; 1 \u0026amp;\u0026amp; c-\u0026gt;d[c-\u0026gt;len - 1] == 0) c-\u0026gt;len--; } 除以小整数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /* 商存入 q，余数通过 rem 传出；b 为普通 int */ void divSmall(const BigInt *a, int b, BigInt *q, int *rem) { init(q); q-\u0026gt;len = a-\u0026gt;len; long long r = 0; // 从最高位开始模拟长除法 for (int i = a-\u0026gt;len - 1; i \u0026gt;= 0; i--) { r = r * 10 + a-\u0026gt;d[i]; q-\u0026gt;d[i] = (int)(r / b); r = r % b; } *rem = (int)r; while (q-\u0026gt;len \u0026gt; 1 \u0026amp;\u0026amp; q-\u0026gt;d[q-\u0026gt;len - 1] == 0) q-\u0026gt;len--; } 完整演示程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 int main(void) { BigInt a, b, c; // ========== 加法演示 ========== fromStr(\u0026amp;a, \u0026#34;99999999999999999999\u0026#34;); /* 20 位 9 */ fromStr(\u0026amp;b, \u0026#34;1\u0026#34;); add(\u0026amp;a, \u0026amp;b, \u0026amp;c); printf(\u0026#34;加法: \u0026#34;); print(\u0026amp;c); /* 输出 10^20 */ // ========== 乘法演示 ========== fromStr(\u0026amp;a, \u0026#34;123456789\u0026#34;); fromStr(\u0026amp;b, \u0026#34;987654321\u0026#34;); mul(\u0026amp;a, \u0026amp;b, \u0026amp;c); printf(\u0026#34;乘法: \u0026#34;); print(\u0026amp;c); /* 输出 121932631112635269 */ // ========== 除法演示 ========== int rem; BigInt q; fromStr(\u0026amp;a, \u0026#34;123456789\u0026#34;); divSmall(\u0026amp;a, 7, \u0026amp;q, \u0026amp;rem); printf(\u0026#34;除法商: \u0026#34;); print(\u0026amp;q); printf(\u0026#34;余数: %d\\n\u0026#34;, rem); return 0; } 应用：计算阶乘 1 2 3 4 5 6 7 8 9 10 11 12 13 14 /* 计算 n! 并打印 */ void factorial(int n) { BigInt result, tmp; char buf[20]; fromStr(\u0026amp;result, \u0026#34;1\u0026#34;); for (int i = 2; i \u0026lt;= n; i++) { sprintf(buf, \u0026#34;%d\u0026#34;, i); fromStr(\u0026amp;tmp, buf); mul(\u0026amp;result, \u0026amp;tmp, \u0026amp;result); } printf(\u0026#34;%d! = \u0026#34;, n); print(\u0026amp;result); } 复杂度与优缺点 时间复杂度 设大数位数为 $n$：\n运算 本文实现 优化上限 加 / 减 $O(n)$ — 乘法 $O(n^2)$ $O(n \\log n)$（FFT） 除以小整数 $O(n)$ — 阶乘 $n!$ $O(n^2 \\cdot \\log n)$ — 空间复杂度 $O(n)$，即结构体中的 d[MAXN] 数组大小。\n优点 原理直观，与手工竖式完全对应，易于理解和调试 纯 C 实现，无任何外部依赖 加减法 $O(n)$，对中等规模（$\\leq 10^4$ 位）完全够用 缺点 乘法 $O(n^2)$，对超大数（$\u0026gt; 10^5$ 位）较慢 每格只存 1 位，常数因子偏大；可改为万进制（每格存 4 位）提速约 4 倍 暂不支持负数，需额外引入符号位处理 万进制优化简介 将 BASE 从 10 改为 10000，每个数组元素存 4 位十进制数：\n1 2 3 4 5 6 7 #define BASE 10000 #define BASEW 4 /* 每格宽度，用于打印补零 */ /* 打印时：最高格不补零，其余各格补足 4 位 */ printf(\u0026#34;%d\u0026#34;, a-\u0026gt;d[a-\u0026gt;len - 1]); for (int i = a-\u0026gt;len - 2; i \u0026gt;= 0; i--) printf(\u0026#34;%04d\u0026#34;, a-\u0026gt;d[i]); 加减乘的逻辑完全相同，只需把所有 % 10 改为 % BASE，/ 10 改为 / BASE。\n","date":"2026-03-15T22:06:00+08:00","permalink":"https://w2343419-del.github.io/WangScape/p/c-%E8%AF%AD%E8%A8%80%E7%9A%84%E9%AB%98%E7%B2%BE%E5%BA%A6%E8%AE%A1%E7%AE%97/","title":"C 语言的高精度计算"},{"content":"算法复杂度完全指南——时间、空间与渐进时间复杂度 复杂度分析是衡量算法效率的核心工具，帮助我们在编写代码前提前预判程序的性能瓶颈。本文将系统地讲解时间复杂度、空间复杂度，以及 $O$、$\\Omega$、$\\Theta$ 三种渐进符号的含义与应用，并配合完整样例加以说明。\n什么是复杂度 当我们评估一个算法的好坏时，不能只看它能否得出正确结果，还要看它在数据量增大时的表现。复杂度就是用来描述\u0026quot;随输入规模 $n$ 增长，算法所需资源的变化趋势\u0026quot;的数学工具。\n时间复杂度：算法需要执行多少步操作？ 空间复杂度：算法需要占用多少额外内存？ 两者都使用渐进符号来表达——忽略常数系数，只关注增长趋势。计算规则如下：\n只保留最高次项：$3n^2 + 2n + 1 \\Rightarrow O(n^2)$ 忽略常数系数：$5n \\Rightarrow O(n)$ 循环嵌套相乘：两层各跑 $n$ 次 $\\Rightarrow O(n^2)$ 顺序结构取最大：$O(n) + O(n^2) \\Rightarrow O(n^2)$ 三种渐进时间复杂度 同一个算法在不同输入下表现可能截然不同。三种渐进时间复杂度分别从上界、下界、紧确界三个角度描述算法的行为边界。\n大 O 符号（上界，最坏情况） 数学定义：存在常数 $c \u0026gt; 0$ 和 $n_0$，当 $n \\geq n_0$ 时，始终有：\n$$f(n) \\leq c \\cdot g(n)$$算法的运行时间最多是 $g(n)$ 的常数倍，是增长速度的上限承诺——保证不会比这更慢。日常开发中使用最广泛，说\u0026quot;这个算法是 $O(n^2)$\u0026ldquo;通常就是指最坏情况。\n1 2 3 4 5 6 7 8 9 // 线性查找 —— O(n) // 最坏情况：目标在最后，遍历全部 n 个元素 int linear_search(int arr[], int n, int target) { for (int i = 0; i \u0026lt; n; i++) { // 最多执行 n 次 if (arr[i] == target) return i; } return -1; } 大 Ω 符号（下界，最好情况） 数学定义：存在常数 $c \u0026gt; 0$ 和 $n_0$，当 $n \\geq n_0$ 时，始终有：\n$$f(n) \\geq c \\cdot g(n)$$算法的运行时间至少是 $g(n)$ 的常数倍，是增长速度的下限承诺——保证不会比这更快。\n1 2 3 4 5 6 7 8 9 // 线性查找 —— Ω(1) // 最好情况：目标就在第一个位置，只执行 1 次 int linear_search(int arr[], int n, int target) { for (int i = 0; i \u0026lt; n; i++) { if (arr[i] == target) return i; // 第一次就命中！ } return -1; } 经典结论：任何基于比较的排序算法，下界都是 $\\Omega(n \\log n)$，这是数学可证明的极限，无法突破。\n大 Θ 符号（紧确界，精确描述） 数学定义：存在常数 $c_1, c_2 \u0026gt; 0$ 和 $n_0$，当 $n \\geq n_0$ 时，始终有：\n$$c_1 \\cdot g(n) \\leq f(n) \\leq c_2 \\cdot g(n)$$算法被 $g(n)$ 从上下两侧同时夹住，是最精确的描述。$\\Theta$ 成立当且仅当 $O$ 和 $\\Omega$ 同时成立且阶数相同。\n1 2 3 4 5 6 7 8 9 10 // 遍历整个数组 —— Θ(n) // 无论输入如何，都必须访问每个元素，不多不少 int find_max(int arr[], int n) { int max_val = arr[0]; for (int i = 1; i \u0026lt; n; i++) { // 精确执行 n-1 次，无法提前退出 if (arr[i] \u0026gt; max_val) max_val = arr[i]; } return max_val; } 三种符号对比 符号 含义 直觉记忆 线性查找举例 $O(g)$ 上界 最慢不超过这个速度 $O(n)$，最坏遍历全部 $\\Omega(g)$ 下界 最快不低于这个速度 $\\Omega(1)$，最好第一个就找到 $\\Theta(g)$ 紧确界 就是这个速度 不存在（上下界不同阶） 线性查找没有 $\\Theta$，因为最好与最坏情况的阶数不同，上下界无法合拢。\n时间复杂度 时间复杂度描述算法执行步骤数随输入规模 $n$ 的增长趋势。\n常见阶数对比 复杂度 名称 典型场景 $n=10^6$ 时的量级 $O(1)$ 常数时间 数组按下标访问、哈希表查找 1 次 $O(\\log n)$ 对数时间 二分查找、平衡二叉树操作 ~20 次 $O(n)$ 线性时间 遍历数组、线性查找 $10^6$ 次 $O(n \\log n)$ 线性对数 归并排序、堆排序 ~$2 \\times 10^7$ 次 $O(n^2)$ 平方时间 冒泡排序、选择排序 $10^{12}$ 次 ⚠️ $O(2^n)$ 指数时间 暴力递归子集枚举 不可接受 🚫 增长速度：$O(1) \u0026lt; O(\\log n) \u0026lt; O(n) \u0026lt; O(n \\log n) \u0026lt; O(n^2) \u0026lt; O(2^n)$\n代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include \u0026lt;stdio.h\u0026gt; /* ========== O(1)：常数时间 ========== */ int get_first(int arr[]) { return arr[0]; // 与 n 无关，直接返回 } /* ========== O(log n)：二分查找 ========== */ int binary_search(int arr[], int n, int target) { int lo = 0, hi = n - 1; while (lo \u0026lt;= hi) { int mid = lo + (hi - lo) / 2; if (arr[mid] == target) return mid; else if (arr[mid] \u0026lt; target) lo = mid + 1; // 每次规模减半 else hi = mid - 1; } return -1; } /* ========== O(n)：线性遍历 ========== */ int linear_sum(int arr[], int n) { int total = 0; for (int i = 0; i \u0026lt; n; i++) // 执行 n 次 total += arr[i]; return total; } /* ========== O(n²)：冒泡排序 ========== */ void bubble_sort(int arr[], int n) { for (int i = 0; i \u0026lt; n; i++) { for (int j = 0; j \u0026lt; n - i - 1; j++) { // 嵌套循环 → n² if (arr[j] \u0026gt; arr[j + 1]) { int tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; } } } } 空间复杂度 空间复杂度描述算法运行时额外占用内存随输入规模的增长趋势（不含输入数据本身）。\n常见阶数对比 复杂度 含义 典型场景 $O(1)$ 固定空间 原地排序、用几个临时变量 $O(\\log n)$ 对数空间 递归调用栈（二分、快排平均） $O(n)$ 线性空间 复制数组、哈希表、BFS 队列 $O(n^2)$ 平方空间 创建 $n \\times n$ 矩阵、邻接矩阵 代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include \u0026lt;stdlib.h\u0026gt; /* ========== O(1)：原地操作 ========== */ int sum_array(int arr[], int n) { int total = 0; // 只有 1 个变量，与 n 无关 for (int i = 0; i \u0026lt; n; i++) total += arr[i]; return total; } /* ========== O(n)：创建新数组 ========== */ int* double_array(int arr[], int n) { int* result = malloc(n * sizeof(int)); // 额外占用 n 个空间 for (int i = 0; i \u0026lt; n; i++) result[i] = arr[i] * 2; return result; } /* ========== O(n)：线性递归调用栈 ========== */ long long factorial(int n) { if (n \u0026lt;= 1) return 1; return n * factorial(n - 1); // 递归深度为 n，栈帧占用 O(n) } /* ========== O(log n)：对数深度递归 ========== */ int binary_search_rec(int arr[], int target, int lo, int hi) { if (lo \u0026gt; hi) return -1; int mid = lo + (hi - lo) / 2; if (arr[mid] == target) return mid; if (arr[mid] \u0026lt; target) return binary_search_rec(arr, target, mid + 1, hi); else return binary_search_rec(arr, target, lo, mid - 1); // 递归深度为 log n，栈帧占用 O(log n) } 递归函数每次调用都会在调用栈上分配一个栈帧，递归深度即为空间复杂度。深度递归在极端情况下可能导致栈溢出。\n综合样例分析：两数之和 用一道经典问题完整演示三种渐进符号与时空复杂度的分析过程。\n问题 问题描述 给定整数数组 arr 和目标值 target，找出数组中和为 target 的两个数的下标。每个输入只有一个答案，不能使用同一元素两次。\n输入输出 输入：arr = [2, 7, 11, 15]，target = 9 输出：[0, 1] 约束条件 $2 \\leq n \\leq 10^4$ $-10^9 \\leq arr[i] \\leq 10^9$ 保证有且只有一个答案 思路分析 解法一：暴力枚举 枚举所有数对 $(i, j)$，检查是否满足 arr[i] + arr[j] == target。思路直接，无需额外空间，但时间效率差。\n解法二：哈希表优化 遍历数组时，将已见过的值存入哈希表。对每个元素，检查其补数（target - arr[i]）是否已在表中。若命中则直接返回，否则将当前元素入表。\n这是典型的用空间换时间：额外花费 $O(n)$ 空间，将时间从 $O(n^2)$ 降至 $O(n)$。\n代码实现 解法一：暴力枚举 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 时间 O(n²)，空间 O(1) // 返回值：result[0], result[1] 为下标，未找到则返回 {-1, -1} void two_sum_brute(int arr[], int n, int target, int result[2]) { result[0] = result[1] = -1; for (int i = 0; i \u0026lt; n; i++) { for (int j = i + 1; j \u0026lt; n; j++) { // 枚举所有数对 if (arr[i] + arr[j] == target) { result[0] = i; result[1] = j; return; } } } } 解法二：哈希表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;string.h\u0026gt; // 简单哈希表节点 typedef struct Node { int key; // 数组值 int val; // 下标 struct Node* next; } Node; #define TABLE_SIZE 10007 // 时间 O(n)，空间 O(n) void two_sum_hash(int arr[], int n, int target, int result[2]) { Node* table[TABLE_SIZE]; memset(table, 0, sizeof(table)); result[0] = result[1] = -1; for (int i = 0; i \u0026lt; n; i++) { int complement = target - arr[i]; // 所需补数 // 查表 int h = ((complement % TABLE_SIZE) + TABLE_SIZE) % TABLE_SIZE; for (Node* p = table[h]; p; p = p-\u0026gt;next) { if (p-\u0026gt;key == complement) { result[0] = p-\u0026gt;val; result[1] = i; return; } } // 当前元素入表 int hi = ((arr[i] % TABLE_SIZE) + TABLE_SIZE) % TABLE_SIZE; Node* node = malloc(sizeof(Node)); node-\u0026gt;key = arr[i]; node-\u0026gt;val = i; node-\u0026gt;next = table[hi]; table[hi] = node; } } 复杂度与优缺点 解法一：暴力枚举 时间：$O(n^2)$（最坏），$\\Omega(1)$（最好，第一对就命中），无 $\\Theta$\n空间：$\\Theta(1)$\n无需额外空间，内存友好\n实现简单，无需哈希函数\n时间效率差，$n = 10^4$ 时已有亿级操作\n不适合大规模数据\n解法二：哈希表 时间：$\\Theta(n)$（必须遍历一次，哈希查找为 $O(1)$）\n空间：$\\Theta(n)$（哈希表最多存 $n$ 个元素）\n时间效率高，线性扫描一次即可\n适合大规模数据\n需要额外 $O(n)$ 内存\n哈希冲突极端情况下可能退化\n对比总结 时间（最坏） 时间（最好） 时间（$\\Theta$） 空间 推荐场景 暴力枚举 $O(n^2)$ $\\Omega(1)$ — $O(1)$ 内存极度受限 哈希表 $O(n)$ $\\Omega(n)$ $\\Theta(n)$ $O(n)$ 一般业务系统 时间与空间的权衡 在实际工程中，时间和空间往往不能同时最优，需要根据场景做出取舍。\n用空间换时间（最常见）：哈希表、缓存、动态规划的记忆化数组。 用时间换空间：流式处理大文件时逐行读取，避免一次性加载全部数据到内存。 场景 推荐策略 实时响应、高并发系统 牺牲空间，优化时间 嵌入式设备、内存受限环境 牺牲时间，节省空间 一般业务系统 优先优化时间，空间够用即可 常见算法复杂度速查 算法 时间（$O$） 时间（$\\Omega$） 时间（$\\Theta$） 空间 数组访问 $O(1)$ $\\Omega(1)$ $\\Theta(1)$ $O(1)$ 线性查找 $O(n)$ $\\Omega(1)$ — $O(1)$ 二分查找 $O(\\log n)$ $\\Omega(1)$ — $O(1)$ 冒泡排序 $O(n^2)$ $\\Omega(n)$ $\\Theta(n^2)$ $O(1)$ 归并排序 $O(n \\log n)$ $\\Omega(n \\log n)$ $\\Theta(n \\log n)$ $O(n)$ 快速排序 $O(n^2)$ $\\Omega(n \\log n)$ — $O(\\log n)$ 哈希表查找 $O(n)$ $\\Omega(1)$ — $O(n)$ 快速排序和线性查找没有 $\\Theta$，因为最好与最坏情况的阶数不同，上下界无法合拢。\n","date":"2026-03-09T10:32:00+08:00","permalink":"https://w2343419-del.github.io/WangScape/p/%E6%97%B6%E9%97%B4%E4%B8%8E%E7%A9%BA%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6/","title":"时间与空间复杂度"},{"content":"在算法题当中，我们经常可以看见动态规划的影子，所以在此总结一下动态规划（DP）和其中非常重要的一个部分——状态转移方程。\n一、何为动态规划 动态规划（Dynamic Programming，DP）是一种通过把原问题分解为子问题来求解的算法思想。\n动态规划不是某种具体的数据结构，而是一种思维方式。\nDP 需要满足以下两个性质：\n1. 最优子结构 原问题的最优解包含子问题的最优解。\n2. 重叠子问题 子问题被反复计算，可以缓存避免重复。\n二、何为状态转移方程 要了解状态转移方程，首先应当知道何为\u0026quot;状态\u0026quot;。\n1. \u0026ldquo;状态\u0026rdquo; 状态是对问题在某一阶段的描述，通常用 dp[i] 或 dp[i][j] 表示。\n例如：\ndp[i] = 前 i 个元素的最优解 dp[i][j] = 从位置 i 到位置 j 的最优解 dp[i][w] = 考虑前 i 个物品、剩余容量为 w 时的最优解 2. 状态转移方程 状态转移方程可以粗略的写成：\n1 新状态 = f(旧状态) 状态转移方程所做的是确定该步有什么选择，以及选择背后所对应的子问题（可以理解为递推式）。\n三、典型例题 例 1：线性 DP - 爬楼梯 问题 每次可以爬 1 或 2 个台阶，爬到第 n 阶有多少种方法？\n思路分析 定义状态：dp[i] = 爬到第 i 阶的方法数 最后一步分析：到达第 i 阶，只能从第 i-1 阶（迈 1 步）或第 i-2 阶（迈 2 步）过来 根据分析，我们可以得到这样一个状态转移方程： $$dp[i] = dp[i-1] + dp[i-2]$$注意：此状态转移方程还有两个边界状态：dp[1] = 1，dp[2] = 2\n图解： 1 2 3 4 5 6 i = 5 dp[1] = 1 dp[2] = 2 dp[3] = 3 dp[4] = 5 dp[5] = 8 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include \u0026lt;stdio.h\u0026gt; int climbStairs(int n) { if (n \u0026lt;= 2) return n; int dp[n + 1]; dp[1] = 1; dp[2] = 2; for (int i = 3; i \u0026lt;= n; i++) { dp[i] = dp[i-1] + dp[i-2]; // 状态转移方程 } return dp[n]; } int main() { printf(\u0026#34;爬 5 阶楼梯的方法数: %d\\n\u0026#34;, climbStairs(5)); // 输出: 8 return 0; } 时间复杂度与优缺点 时间复杂度 ：$O(n)$\n空间复杂度 ：$O(n)$\n优点 ：\n问题简单，易于理解 状态定义直观 缺点 ：\n只能计算一固定地方的值，若要计算多次则需多次递推 例 2：线性 DP - 打家劫舍 问题 一排房子不能抢相邻的，求最大金额。给定数组 nums 表示各房子的金额。\n思路分析 定义状态：dp[i] = 抢到第 i 间房能获得的最大金额 最后一步分析：第 i 间房，要么抢，要么不抢 抢这间：得到 nums[i] + dp[i-2]（前面最多抢到 i-2 间） 不抢这间：得到 dp[i-1]（抢到 i-1 间的最大值） 状态转移方程： $$dp[i] = \\max(dp[i-1], dp[i-2] + nums[i])$$ 图解： 1 2 3 4 5 6 nums = [2, 7, 9, 3, 1] dp[0] = 2 dp[1] = max(2, 7) = 7 dp[2] = max(7, 2+9) = 11 dp[3] = max(11, 7+3) = 11 dp[4] = max(11, 11+1) = 12 (抢第 0、2、4 间：2+9+1=12) 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include \u0026lt;stdio.h\u0026gt; int max(int a, int b) { return a \u0026gt; b ? a : b; } int rob(int *nums, int n) { if (n == 1) return nums[0]; int dp[n]; dp[0] = nums[0]; dp[1] = max(nums[0], nums[1]); for (int i = 2; i \u0026lt; n; i++) { dp[i] = max(dp[i-1], dp[i-2] + nums[i]); // 状态转移方程 } return dp[n-1]; } int main() { int nums[] = {2, 7, 9, 3, 1}; int n = sizeof(nums) / sizeof(nums[0]); printf(\u0026#34;最大金额: %d\\n\u0026#34;, rob(nums, n)); // 输出: 12 return 0; } 时间复杂度与优缺点 时间复杂度 ：$O(n)$\n空间复杂度 ：$O(n)$，可优化为 $O(1)$（仅保留前两项）\n优点 ：\n✅ 与爬楼梯类似，思路清晰 ✅ 可优化空间至 O(1) 缺点 ：\n❌ 不能直接回溯具体哪些房子被抢 例 3：背包 DP - 0/1 背包 问题 n 个物品，重量 w[]，价值 v[]，背包容量 W，求最大价值。\n思路分析 定义状态：dp[i][j] = 考虑前 i 个物品、容量为 j 时的最大价值 最后一步分析：第 i 个物品，放或不放 不放：dp[i][j] = dp[i-1][j] 放：dp[i][j] = dp[i-1][j - w[i]] + v[i]（需 $j \\geq w[i]$） 状态转移方程： $$dp[i][j] = \\max(dp[i-1][j], dp[i-1][j - w[i]] + v[i])$$ 图解： 物品：(w=2, v=3)、(w=3, v=4)、(w=4, v=5) 背包容量 W=5\n1 2 3 4 5 6 7 j=0 j=1 j=2 j=3 j=4 j=5 i=0 0 0 0 0 0 0 i=1 0 0 3 3 3 3 i=2 0 0 3 4 4 7 ← 放物品 1 + 物品 2，价值=3+4=7 i=3 0 0 3 4 5 7 答案：dp[3][5] = 7 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include \u0026lt;stdio.h\u0026gt; #define MAX_N 105 #define MAX_W 1005 int dp[MAX_N][MAX_W]; int max(int a, int b) { return a \u0026gt; b ? a : b; } int knapsack(int *w, int *v, int n, int W) { for (int i = 0; i \u0026lt;= n; i++) for (int j = 0; j \u0026lt;= W; j++) dp[i][j] = 0; for (int i = 1; i \u0026lt;= n; i++) { for (int j = 0; j \u0026lt;= W; j++) { dp[i][j] = dp[i-1][j]; // 不放 if (j \u0026gt;= w[i-1]) // 放得下 dp[i][j] = max(dp[i][j], dp[i-1][j - w[i-1]] + v[i-1]); // 状态转移方程 } } return dp[n][W]; } int main() { int w[] = {2, 3, 4}; int v[] = {3, 4, 5}; int n = 3, W = 5; printf(\u0026#34;最大价值: %d\\n\u0026#34;, knapsack(w, v, n, W)); // 输出: 7 return 0; } 空间优化 ：二维 dp 可以压缩成一维，内层循环必须倒序，防止同一件物品被重复放入：\n1 2 3 for (int i = 0; i \u0026lt; n; i++) for (int j = W; j \u0026gt;= w[i]; j--) // 倒序！ dp[j] = max(dp[j], dp[j - w[i]] + v[i]); 时间复杂度与优缺点 时间复杂度 ：$O(nW)$\n空间复杂度 ：$O(nW)$，优化后为 $O(W)$\n优点 ：\nDP 框架经典，易于扩展 可一次性解决所有容量的最优值 缺点 ：\n物品数量或容量很大时，时间空间压力大 例 4：序列 DP - 最长公共子序列（LCS） 问题 两个字符串，求最长公共子序列的长度。例：\u0026quot;abcde\u0026quot; 和 \u0026quot;ace\u0026quot; → 长度为 3（ace）\n思路分析 定义状态：dp[i][j] = s1 前 i 个字符与 s2 前 j 个字符的 LCS 长度\n最后一步分析：s1[i] 和 s2[j] 是否相等：\n相等：dp[i][j] = dp[i-1][j-1] + 1 不相等：dp[i][j] = max(dp[i-1][j], dp[i][j-1])（去掉 s1[i] 或 s2[j]，看哪个更优） 状态转移方程：当 s1[i-1] == s2[j-1] 时，dp[i][j] = dp[i-1][j-1] + 1；否则 dp[i][j] = max(dp[i-1][j], dp[i][j-1])\n或用分段函数表示：\n$$dp[i][j] = \\begin{cases} \\text{dp[i-1][j-1] + 1} \u0026 \\text{相等时} \\\\ \\text{max(dp[i-1][j], dp[i][j-1])} \u0026 \\text{不相等时} \\end{cases}$$ 图解： s1 = \u0026ldquo;abcde\u0026rdquo; s2 = \u0026ldquo;ace\u0026rdquo;\n1 2 3 4 5 6 7 \u0026#34;\u0026#34; a c e \u0026#34;\u0026#34; 0 0 0 0 a 0 1 1 1 b 0 1 1 1 c 0 1 2 2 d 0 1 2 2 e 0 1 2 3 ← 得解 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; #define MAX_LEN 105 int dp[MAX_LEN][MAX_LEN]; int max(int a, int b) { return a \u0026gt; b ? a : b; } int lcs(char *s1, char *s2) { int m = strlen(s1); int n = strlen(s2); for (int i = 0; i \u0026lt;= m; i++) dp[i][0] = 0; for (int j = 0; j \u0026lt;= n; j++) dp[0][j] = 0; for (int i = 1; i \u0026lt;= m; i++) { for (int j = 1; j \u0026lt;= n; j++) { if (s1[i-1] == s2[j-1]) dp[i][j] = dp[i-1][j-1] + 1; // 字符匹配 else dp[i][j] = max(dp[i-1][j], dp[i][j-1]); // 状态转移方程 } } return dp[m][n]; } int main() { char s1[] = \u0026#34;abcde\u0026#34;; char s2[] = \u0026#34;ace\u0026#34;; printf(\u0026#34;LCS 长度: %d\\n\u0026#34;, lcs(s1, s2)); // 输出: 3 return 0; } 时间复杂度与优缺点 时间复杂度 ：$O(mn)$\n空间复杂度 ：$O(mn)$，可优化为 $O(\\min(m,n))$（滚动数组）\n优点 ：\n框架适用于序列对齐问题 可扩展到 LCS 具体字符（回溯） 缺点 ：\nm、n 都很大时空间压力大 例 5：区间 DP - 戳气球 问题 戳破气球 i 得分 = nums[i-1] * nums[i] * nums[i+1]，求最大总得分。\n思路分析 关键思路 ：不想\u0026quot;先戳哪个\u0026quot;，而想\u0026quot;区间 (i,j) 中最后一个戳哪个\u0026quot;，这样两侧边界已知，避免状态混乱\n定义状态：dp[i][j] = 戳破开区间 (i,j) 内所有气球的最大得分\n根据分析，我们可以得到这样一个状态转移方程：\n$$dp[i][j] = \\max_{i \u003c k \u003c j}(dp[i][k] + dp[k][j] + nums[i] \\times nums[k] \\times nums[j])$$其中 k 是区间 (i,j) 中最后一个被戳的气球。\n代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; #define MAX_N 305 int dp[MAX_N][MAX_N]; int nums[MAX_N]; int max(int a, int b) { return a \u0026gt; b ? a : b; } int maxCoins(int *arr, int n) { // 加哨兵边界 nums[0] = 1; for (int i = 1; i \u0026lt;= n; i++) nums[i] = arr[i-1]; nums[n+1] = 1; int N = n + 2; memset(dp, 0, sizeof(dp)); // 按区间长度从小到大枚举 for (int len = 2; len \u0026lt; N; len++) { for (int i = 0; i \u0026lt; N - len; i++) { int j = i + len; for (int k = i+1; k \u0026lt; j; k++) { // k 是最后被戳的气球 int score = dp[i][k] + dp[k][j] + nums[i] * nums[k] * nums[j]; dp[i][j] = max(dp[i][j], score); // 状态转移方程 } } } return dp[0][N-1]; } int main() { int arr[] = {3, 1, 5, 8}; int n = sizeof(arr) / sizeof(arr[0]); printf(\u0026#34;最大得分: %d\\n\u0026#34;, maxCoins(arr, n)); // 输出: 167 return 0; } 时间复杂度与优缺点 时间复杂度 ：$O(n^3)$\n空间复杂度 ：$O(n^2)$\n优点 ：\n区间 DP 的经典例题 \u0026ldquo;最后一个\u0026quot;的思路极具启发性 可通过回溯得到具体戳的顺序 缺点 ：\n思路相对复杂，初次接触易困惑 时间复杂度为立方级 四、DP 模型总结 DP 模型对比总结：\n类型 状态转移方程 时间复杂度 空间复杂度 代表问题 线性 DP 递推关系 $O(n)$ $O(n)$ 爬楼梯、打家劫舍 背包 DP 选择最大值 $O(nW)$ $O(nW)$ 0/1 背包、完全背包 序列 DP 两指针递推 $O(mn)$ $O(mn)$ LCS、编辑距离 区间 DP 区间分割递推 $O(n^3)$ $O(n^2)$ 戳气球、矩阵链乘 总结与建议 从问题出发 ：确定能否用 DP（有最优子结构和重叠子问题） 定义状态 ：清晰地定义 dp[...] 的含义 写出转移方程 ：确定状态间的关系 确定初始条件 ：边界情况的处理 实现与优化 ：代码实现，再考虑空间+时间优化 ","date":"2026-03-02T13:57:00+08:00","permalink":"https://w2343419-del.github.io/WangScape/p/%E7%8A%B6%E6%80%81%E8%BD%AC%E7%A7%BB%E6%96%B9%E7%A8%8B%E4%B8%8E%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/","title":"状态转移方程与动态规划"},{"content":"这是一道经典但具有一定难度的棋盘模型题。虽然是 2000 年的 NOIP 题目，但作为压轴题，对于第一次接触的人来说还是相当困难的。本文总结了三种不同的解法：动态规划、DFS 记忆化和最小费用最大流，从易到难逐步展开。\n问题 题目来源 NOIP 2000 提高组 T4\n问题描述 设有 N×N 的方格图 (N≤9)，我们将其中的某些方格中填入正整数，而其他的方格中则放入数字 0。 某人从图的左上角的 A 点（0，0）出发，可以向下行走，也可以向右走，直到到达右下角的 B 点（N, N)。在走过的路上，他可以取走方格中的数（取走后的方格中将变为数字 0）。 此人从 A 点到 B 点共走两次，试找出 2 条这样的路径，使得取得的数之和为最大。\n输入输出 输入格式 ：输入的第一行为一个整数 N（表示 N×N 的方格图），接下来的每行有三个整数，前两个表示位置，第三个数为该位置上所放的数。一行单独的 0 表示输入结束。\n输出格式 ：只需输出一个整数，表示 2 条路径上取得的最大的和。\n样例 输入：\n1 2 3 4 5 6 7 8 9 10 8 2 3 13 2 6 6 3 5 7 4 4 14 5 2 21 5 6 4 6 3 15 7 2 14 0 0 0 输出：\n1 67 约束条件 数据范围：1≤N≤9 问题分析 为什么不能采取两次枚举（即先找一条最优路径，再在剩余格子中找第二条）？因为一次路径会改变地图（取走数字），影响第二次的结果，所以两条路径必须联动考虑，而不能独立优化。\n思路分析 三种解法的思路分别如下：\n解法一：动态规划 核心思想 ：将两条路径同步推进，用一个 DP 同时模拟两个人的行走。\n状态设计 ：设 dp[k][x1][x2]，其中：\nk 为当前走的步数（即 x + y 的值，从 2 到 2N） x1、x2 分别为两人当前所在的行号 由 k 和 x 可以推出 y = k - x（关键优化，这一步降低了维度），因此列号不需要单独存储 去重处理 ：当两人在同一格时（x1 == x2，则 y1 == y2），该格只取一次。\n状态转移 ：每步两人各自可以选择向右或向下，共 4 种组合。每个状态 dp[k][x1][x2] 代表走到第 k 步，人 1 在行 x1、人 2 在行 x2 时的最大取数和。\n解法二：DFS + 记忆化搜索 核心思想 ：类似于 DP 算法（毕竟深搜和 DP 本质上就是一个东西），但添加了记忆化搜索来避免重复计算。若无记忆化搜索，计算量将是指数级爆炸，在 N=9 时约为 $4^{16}$ 次。\n实现方式 ：从初始状态出发，递归地尝试所有可能的转移，同时用 memo 数组缓存已计算过的状态，避免重复。在达到终点时返回 0，逐层返回最大值。\n解法三：费用流（最小费用最大流） 核心思想 ：将\u0026quot;两条路径取最大值\u0026quot;转化为网络流问题。\n建模思路 ：\n两条从 A 到 B 的路径 = 从源点到汇点流量为 2 的流 每个格子最多取一次 = 每个节点容量限制 取得数字最大 = 费用最大（转为最小费用取负值） 拆点处理 ：每个格子 (i,j) 拆成两个节点 in 和 out：\nin → out 容量 1，费用 -map[i][j]（第一条路径取数） in → out 再加容量 1，费用 0（第二条路径经过但不取数） 连边 ：(i,j) 的 out 连向 (i+1,j) 的 in 和 (i,j+1) 的 in，容量 2，费用 0。\n代码实现 解法一：动态规划 完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include \u0026lt;stdio.h\u0026gt; int N; int map[10][10]; int dp[20][10][10]; // dp[步数][人1的行][人2的行] int main() { scanf(\u0026#34;%d\u0026#34;, \u0026amp;N); int x, y, v; while (scanf(\u0026#34;%d %d %d\u0026#34;, \u0026amp;x, \u0026amp;y, \u0026amp;v) \u0026amp;\u0026amp; (x || y || v)) map[x][y] = v; // 初始化为-1表示不可达 for (int k = 0; k \u0026lt; 20; k++) for (int i = 0; i \u0026lt; 10; i++) for (int j = 0; j \u0026lt; 10; j++) dp[k][i][j] = -1; dp[2][1][1] = map[1][1]; // ========== 动态规划主循环 ========== for (int k = 2; k \u0026lt; 2 * N; k++) { for (int x1 = 1; x1 \u0026lt;= N; x1++) { int y1 = k - x1; if (y1 \u0026lt; 1 || y1 \u0026gt; N) continue; for (int x2 = x1; x2 \u0026lt;= N; x2++) { int y2 = k - x2; if (y2 \u0026lt; 1 || y2 \u0026gt; N) continue; if (dp[k][x1][x2] \u0026lt; 0) continue; // 尝试所有4种移动组合 for (int m1 = 0; m1 \u0026lt;= 1; m1++) { for (int m2 = 0; m2 \u0026lt;= 1; m2++) { int nx1 = x1 + m1, ny1 = y1 + (1 - m1); int nx2 = x2 + m2, ny2 = y2 + (1 - m2); if (nx1 \u0026gt; N || ny1 \u0026gt; N) continue; if (nx2 \u0026gt; N || ny2 \u0026gt; N) continue; // 计算当前步收益 int gain = map[nx1][ny1]; if (nx1 != nx2) gain += map[nx2][ny2]; // 规范化：保证 a \u0026lt;= b int a = nx1, b = nx2; if (a \u0026gt; b) { int t = a; a = b; b = t; } int newval = dp[k][x1][x2] + gain; if (newval \u0026gt; dp[k + 1][a][b]) dp[k + 1][a][b] = newval; } } } } } printf(\u0026#34;%d\\n\u0026#34;, dp[2 * N][N][N]); return 0; } 解法二：DFS + 记忆化搜索 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; // ========== DFS + 记忆化搜索 ========== int N; int map[10][10]; int memo[20][10][10]; // 记忆化数组 int visited[20][10][10]; // 标记是否计算过 int dfs(int k, int x1, int x2) { if (x1 == N \u0026amp;\u0026amp; x2 == N) return 0; if (visited[k][x1][x2]) return memo[k][x1][x2]; visited[k][x1][x2] = 1; int y1 = k - x1; int y2 = k - x2; int best = -1; for (int m1 = 0; m1 \u0026lt;= 1; m1++) { for (int m2 = 0; m2 \u0026lt;= 1; m2++) { int nx1 = x1 + m1, ny1 = y1 + (1 - m1); int nx2 = x2 + m2, ny2 = y2 + (1 - m2); if (nx1 \u0026gt; N || ny1 \u0026gt; N) continue; if (nx2 \u0026gt; N || ny2 \u0026gt; N) continue; int a = nx1, b = nx2; if (a \u0026gt; b) { int t = a; a = b; b = t; } int sub = dfs(k + 1, a, b); if (sub \u0026lt; 0) continue; int gain = map[nx1][ny1]; if (nx1 != nx2) gain += map[nx2][ny2]; if (gain + sub \u0026gt; best) best = gain + sub; } } memo[k][x1][x2] = best; return best; } int main() { scanf(\u0026#34;%d\u0026#34;, \u0026amp;N); int x, y, v; while (scanf(\u0026#34;%d %d %d\u0026#34;, \u0026amp;x, \u0026amp;y, \u0026amp;v) \u0026amp;\u0026amp; (x || y || v)) map[x][y] = v; memset(visited, 0, sizeof(visited)); int start_val = map[1][1]; int result = dfs(2, 1, 1); printf(\u0026#34;%d\\n\u0026#34;, result \u0026gt;= 0 ? start_val + result : 0); return 0; } 解法三：费用流（最小费用最大流） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; #define MAXN 1000 #define MAXE 10000 #define INF 0x3f3f3f3f // ========== 链式前向星数据结构 ========== int head[MAXN], nxt[MAXE], to[MAXE], cap[MAXE], cost[MAXE], tot; void init() { memset(head, -1, sizeof(head)); tot = 0; } void add_edge(int u, int v, int c, int w) { to[tot] = v; cap[tot] = c; cost[tot] = w; nxt[tot] = head[u]; head[u] = tot++; to[tot] = u; cap[tot] = 0; cost[tot] = -w; nxt[tot] = head[v]; head[v] = tot++; } int dist[MAXN], in_queue[MAXN], prevv[MAXN], preve[MAXN]; int queue[MAXN * 100]; // ========== SPFA算法 ========== int spfa(int s, int t, int n) { memset(dist, 0x3f, sizeof(int) * (n + 1)); memset(in_queue, 0, sizeof(int) * (n + 1)); dist[s] = 0; int front = 0, rear = 0; queue[rear++] = s; in_queue[s] = 1; // SPFA主循环 while (front != rear) { int u = queue[front++]; in_queue[u] = 0; // 松弛边 for (int e = head[u]; e != -1; e = nxt[e]) { if (cap[e] \u0026gt; 0 \u0026amp;\u0026amp; dist[to[e]] \u0026gt; dist[u] + cost[e]) { dist[to[e]] = dist[u] + cost[e]; prevv[to[e]] = u; preve[to[e]] = e; if (!in_queue[to[e]]) { queue[rear++] = to[e]; in_queue[to[e]] = 1; } } } } return dist[t] \u0026lt; INF; } // ========== 最小费用最大流 ========== int mcmf(int s, int t, int n) { int total_cost = 0; while (spfa(s, t, n)) { int flow = INF; for (int v = t; v != s; v = prevv[v]) if (cap[preve[v]] \u0026lt; flow) flow = cap[preve[v]]; for (int v = t; v != s; v = prevv[v]) { cap[preve[v]] -= flow; cap[preve[v] ^ 1] += flow; } total_cost += dist[t] * flow; } return total_cost; } int main() { int N; scanf(\u0026#34;%d\u0026#34;, \u0026amp;N); int map[10][10] = {0}; int x, y, v; while (scanf(\u0026#34;%d %d %d\u0026#34;, \u0026amp;x, \u0026amp;y, \u0026amp;v) \u0026amp;\u0026amp; (x || y || v)) map[x][y] = v; init(); int S = 2 * N * N + 1; int T = 2 * N * N + 2; int total_nodes = T; #define IN(i,j) ((i-1)*N+(j)) #define OUT(i,j) (N*N+(i-1)*N+(j)) for (int i = 1; i \u0026lt;= N; i++) { for (int j = 1; j \u0026lt;= N; j++) { if (map[i][j] \u0026gt; 0) { add_edge(IN(i,j), OUT(i,j), 1, -map[i][j]); add_edge(IN(i,j), OUT(i,j), 1, 0); } else { add_edge(IN(i,j), OUT(i,j), 2, 0); } if (j + 1 \u0026lt;= N) add_edge(OUT(i,j), IN(i,j+1), 2, 0); if (i + 1 \u0026lt;= N) add_edge(OUT(i,j), IN(i+1,j), 2, 0); } } add_edge(S, IN(1,1), 2, 0); add_edge(OUT(N,N), T, 2, 0); int ans = -mcmf(S, T, total_nodes); printf(\u0026#34;%d\\n\u0026#34;, ans); return 0; } 时间复杂度与优缺点 解法一：动态规划 时间复杂度 ：$O(N^3)$\n状态数：$O(N^2) \\times O(N^2) / 2 = O(N^4)$，但由于降维和 x1、x2 的约束，实际为 $O(N^3)$ 每个状态有 4 次转移 空间复杂度 ：$O(N^3)$\ndp 数组大小为 $2N \\times N \\times N$ 优点 ：\n思路清晰，易于理解 一次遍历解决问题 代码相对简洁 缺点 ：\n空间占用较大（N=9 时约 1.3MB） 解法二：DFS + 记忆化搜索 时间复杂度 ：$O(N^3)$\n状态数与 DP 相同 每个状态最多计算一次（记忆化） 空间复杂度 ：$O(N^3)$\nmemo 和 visited 数组各占 $O(N^3)$ 优点 ：\n逻辑自然，从上向下思考 易于添加剪枝（虽然此题中剪枝不多） 可灵活调整状态定义 缺点 ：\n递归调用栈深度为 $O(N)$（栈空间） 空间复杂度与 DP 相同 解法三：费用流 时间复杂度 ：$O(Flow \\times SPFA) = O(2 \\times E \\log V) = O(N^2 \\times N^2) = O(N^4)$\n流量为 2 SPFA 每次 $O(V \\log V)$，其中 $V = O(N^2)$，$E = O(N^2)$ 空间复杂度 ：$O(V + E) = O(N^2)$\n图的存储空间 优点 ：\n适用于更一般的场景（多条路径、带限制的图等） 代码框架可复用于其他费用流问题 空间占用相对较少 缺点 ：\n时间复杂度最高（约 $O(N^4)$ vs $O(N^3)$） 代码长且复杂，易出错 难度陡升，超出竞赛难度范围 对比与总结 特性 解法一 DP 解法二 DFS 解法三 费用流 易理解度 ★★★★☆ ★★★★☆ ★☆☆☆☆ 实现难度 ★★☆☆☆ ★★☆☆☆ ★★★★★ 时间复杂度 $O(N^3)$ $O(N^3)$ $O(N^4)$ 空间复杂度 $O(N^3)$ $O(N^3)$ $O(N^2)$ 推荐指数 ★★★★★ ★★★★☆ ★★☆☆☆ 结论 ：对于此题，解法一（DP） 是最佳选择，既清晰高效又不过度复杂。解法二适合想要练习 DFS 的同学。解法三虽然优雅，但对于 N≤9 的规模效率不如 DP，仅作扩展知识。\n","date":"2026-02-28T11:31:00+08:00","permalink":"https://w2343419-del.github.io/WangScape/p/p1004-noip-2000-%E6%8F%90%E9%AB%98%E7%BB%84-%E6%96%B9%E6%A0%BC%E5%8F%96%E6%95%B0-%E5%88%86%E6%9E%90%E4%B8%8E%E6%80%BB%E7%BB%93/","title":"P1004 [NOIP 2000 提高组] 方格取数 分析与总结"},{"content":"经过了近一天的半天自我怀疑、半天与 AI 搏斗（doge）和全天用头与桌子比硬度\u0026hellip;\u0026hellip;\n2026 年 1 月 21 日最终成为了一个对我而言非同寻常的日子。\n我的个人博客终于上线了！！！\n博客的目标 我将在这里记录：\n编程中遇到的困难、收获和知识点总结 学习心得和算法分析 偶尔的牢骚和书评 喜欢的诗词和其他文化内容 也许这个博客还会成为我的全学科笔记？（至少在大学期间）\n除了代码方面，也许还会涉及人工智能、游戏、音乐、电影等多个领域。我会尽力让博客以学习为主。\n写在末尾 虽然我不知道能否一直维护这个博客（毕竟有点懒），但会尽力而为。\n希望我的更新频率不会太低\u0026hellip;\u0026hellip;\n（ps. 现在的博客还比较粗糙，但以后会更好的！）\n编辑于 2026 年 2 月 3 日\n","date":"2026-02-03T13:28:00+08:00","permalink":"https://w2343419-del.github.io/WangScape/p/%E9%AB%98%E5%B1%B1%E6%B5%81%E6%B0%B4%E8%A7%85%E7%9F%A5%E9%9F%B3/","title":"高山流水，觅知音"}]