0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1464,日期见页头。两边不一致时信原站。
题目描述
对于一个递归函数 w(a, b, c):
- 如果
a ≤ 0或b ≤ 0或c ≤ 0就返回值 1。 - 如果
a > 20或b > 20或c > 20就返回w(20, 20, 20)。 - 如果
a < b并且b < c就返回w(a,b,c-1) + w(a,b-1,c-1) − w(a,b-1,c)。 - 其它的情况就返回
w(a-1,b,c) + w(a-1,b-1,c) + w(a-1,b,c-1) − w(a-1,b-1,c-1)。
这是个简单的递归函数,但实现起来可能会有些问题。当 a、b、c 均为 15 时, 调用的次数将非常的多。你要想个办法才行。
注意:例如 w(30, -1, 0) 又满足条件 1 又满足条件 2,
请按照最上面的条件来算,答案为 1。
输入格式:会有若干行,并以 -1 -1 -1 结束。
输出格式:输出若干行,每一行格式 w(a, b, c) = ans。注意空格。
数据范围:保证输入的数在 [-2⁶³, 2⁶³ - 1] 之间,并且是整数。
不包括 -1 -1 -1 的输入行数 T 满足 1 ≤ T ≤ 10⁵。
输入输出样例
输入
1 1 1 2 2 2 -1 -1 -1
输出
w(1, 1, 1) = 2 w(2, 2, 2) = 4
样例只有两行。注意输出格式里逗号后面有一个空格。
上面那段输出是仓库里的 p1464.cpp 真跑出来的。
这道题考什么:把题面给的递归一字不差抄进代码,再用记忆化跑得动。 题面没有给算法,只给了四条规则,全部分数都在「抄得对、跑得动、读得对」上。
最容易错的三处
- 抄规则时数错项数。规则 3 是三项,规则 4 是四项。规则 3 当成四项,
w(5,5,5)会从 32 变成 124,程序不报任何错。 抄完逐项数一遍加号和减号。 - 判断的顺序。
w(30,-1,0)既满足「≤ 0」又满足「> 20」,题面说按最上面的条件算。 题面专门举例的地方,就是坑的位置。 - 读入要用
long long,表要跨询问保留:w是纯函数,表留着,问得越多越划算。
能 AC 的:第 ③ 版(顺序照题面、记忆化、表跨询问保留)。
下面每一步里灰色的「深入」块是折叠的:那是这些结论怎么实测出来的,第一遍可以跳过。
1先手算一遍:w(1,1,1) 凭什么等于 2
★ 这一步不能跳。 题面里的 w 没有任何现实意义 —— 它不是「求什么」,
它就是那四条规则本身。这种题读一遍是读不懂的,
唯一的办法是:挑最小的一组,照着规则手算一次。
样例第一行就是 w(1, 1, 1),答案 2。它为什么是 2?照规则从上往下问一遍:
① 三个数都 > 0 吗? 是 -> 规则 1 不管它
② 有谁 > 20 吗? 没有 -> 规则 2 不管它
③ a < b 并且 b < c? 1 < 1 不成立 -> 规则 3 不管它
⇒ 落到规则 4
于是它按规则 4 摊开成四项,而这四项里每一个都有 0,全部直接命中规则 1 返回 1:
w(1,1,1) = w(0,1,1) + w(0,0,1) + w(0,1,0) - w(0,0,0) <- 规则 4
w(0,1,1) = 1 <- 规则 1(有一个数 <= 0)
w(0,0,1) = 1 <- 规则 1(有一个数 <= 0)
w(0,1,0) = 1 <- 规则 1(有一个数 <= 0)
w(0,0,0) = 1 <- 规则 1(有一个数 <= 0)
w(1,1,1) = 1 + 1 + 1 - 1 = 2
再往上一层,形状一模一样 —— w(2,2,2) 的四项各自又摊成上面这种四项:
w(2,2,2) = w(1,2,2) + w(1,1,2) + w(1,2,1) - w(1,1,1) <- 规则 4
w(1,2,2) = 2
w(1,1,2) = 2
w(1,2,1) = 2
w(1,1,1) = 2
w(2,2,2) = 2 + 2 + 2 - 2 = 4
// P1464 —— 把 w 的展开打出来看:这个函数到底在算什么,以及为什么它慢//// 输入:无// 输出:三段// ① w(1,1,1) 的完整展开(四项都直接命中规则 1,手算就能跟完)// ② w(2,2,2) 的完整展开(多了一层,形状和 ① 一样)// ③ 算 w(5,5,5) 时,同一个 (a,b,c) 被**重新算了多少遍** —— 这就是记忆化的全部理由//// ★ 这份程序不解题,它只回答一个问题:**看不懂题面的时候,先手算一次。**// w(a,b,c) 没有任何现实意义,它就是四条规则;而四条规则里到底走哪一条,// 只有把一层层展开摆出来才看得见。//// ⚠ 规则的判定顺序和正解完全一致(规则 1 排最前)—— 否则这张展开图就是错的。
#include <bits/stdc++.h>using namespace std;
/** 判断 (a,b,c) 该走哪条规则,返回 1~4。 */int ruleOf(int a, int b, int c) { if (a <= 0 || b <= 0 || c <= 0) return 1; if (a > 20 || b > 20 || c > 20) return 2; if (a < b && b < c) return 3; return 4;}
/** * 规则 3 / 4 各自的子问题,连同它前面的符号。 * ⚠ 两条规则的项数**不一样**:规则 3 是三项(两加一减),规则 4 是四项(三加一减)。 * 最早写这份程序时把规则 3 也当成四项,w(5,5,5) 就成了 124(正确是 32)—— 展开图会整张骗人。 */vector<pair<int, array<int, 3>>> childrenOf(int a, int b, int c) { if (ruleOf(a, b, c) == 3) return {{+1, {a, b, c - 1}}, {+1, {a, b - 1, c - 1}}, {-1, {a, b - 1, c}}}; return {{+1, {a - 1, b, c}}, {+1, {a - 1, b - 1, c}}, {+1, {a - 1, b, c - 1}}, {-1, {a - 1, b - 1, c - 1}}};}
long long w(int a, int b, int c) { int r = ruleOf(a, b, c); if (r == 1) return 1; if (r == 2) return w(20, 20, 20); long long s = 0; for (auto& [sign, t] : childrenOf(a, b, c)) s += sign * w(t[0], t[1], t[2]); return s;}
string name(int a, int b, int c) { return "w(" + to_string(a) + "," + to_string(b) + "," + to_string(c) + ")";}
/** 缩进打印一棵展开树。depth 只是缩进用;叶子(规则 1)直接打 = 1。 */void show(int a, int b, int c, int depth) { string pad(depth * 2, ' '); int r = ruleOf(a, b, c); if (r == 1) { cout << pad << name(a, b, c) << " = 1 <- 规则 1(有一个数 <= 0)\n"; return; } auto ch = childrenOf(a, b, c); cout << pad << name(a, b, c) << " = "; for (size_t i = 0; i < ch.size(); i++) { cout << (i == 0 ? "" : (ch[i].first < 0 ? " - " : " + ")) << name(ch[i].second[0], ch[i].second[1], ch[i].second[2]); } cout << " <- 规则 " << r << "\n"; for (auto& [sign, t] : ch) show(t[0], t[1], t[2], depth + 1); cout << pad << name(a, b, c) << " = " << w(a, b, c) << "\n";}
int main() { cout << "① 展开 w(1,1,1):四项全都撞上规则 1,一步到底\n\n"; show(1, 1, 1, 0);
cout << "\n② 展开 w(2,2,2):多一层,形状和上面一模一样\n\n"; show(2, 2, 2, 0);
// ③ 子问题重叠:算 w(5,5,5) 的过程里,同一个 (a,b,c) 被重新算了几遍 map<array<int, 3>, long long> cnt; // 这里不能调用上面的 w()(它不记次数),单独写一份带计数的 function<long long(int, int, int)> count = [&](int a, int b, int c) -> long long { cnt[{a, b, c}]++; int r = ruleOf(a, b, c); if (r == 1) return 1; if (r == 2) return count(20, 20, 20); long long s = 0; for (auto& [sign, t] : childrenOf(a, b, c)) s += sign * count(t[0], t[1], t[2]); return s; }; long long ans = count(5, 5, 5);
long long total = 0; for (auto& [k, v] : cnt) total += v; vector<pair<long long, array<int, 3>>> top; for (auto& [k, v] : cnt) top.push_back({v, k}); sort(top.rbegin(), top.rend());
cout << "\n③ 算 w(5,5,5) = " << ans << " 的过程里,一共调用了 " << total << " 次 w(),\n"; cout << " 而不同的 (a,b,c) 只有 " << cnt.size() << " 个。被重算最多的五个:\n\n"; for (int i = 0; i < 5 && i < (int)top.size(); i++) { cout << " " << name(top[i].second[0], top[i].second[1], top[i].second[2]) << " 被算了 " << top[i].first << " 遍\n"; } cout << "\n " << total << " 次调用 / " << cnt.size() << " 个不同的子问题" << " —— 重复的那些,就是记忆化省下来的。\n"; return 0;}点「运行 ▶」看结果
- 规则 3 是三项:
w(a,b,c-1) + w(a,b-1,c-1) − w(a,b-1,c) - 规则 4 是四项:
w(a-1,b,c) + w(a-1,b-1,c) + w(a-1,b,c-1) − w(a-1,b-1,c-1)
⚠ 写这一页那份 p1464Trace.cpp 时就把规则 3 也当成了四项 ——
w(5,5,5) 当场从 32 变成 124,而程序不报任何错。
⇒ 抄题面给的递归时,逐项数一遍加号和减号,比读三遍题都管用。
2先看清楚:这道题不用你想算法
递归是题面直接给的,四条规则一字不差抄进代码就行。所以这道题考的完全不是「想出解法」, 而是三件很具体的事:
① 跑得动吗 四条规则照抄,w(15,15,15) 要调用多少次?
② 规则的顺序对吗 题面专门举了 w(30,-1,0) 这个例子 —— 那句话就是分数
③ 读得对吗 输入范围到 2^63,用 int 读会悄悄截断
★ 这三件事没有一件是「算法」,但它们决定了这道题的全部分数。
3第 ① 版:把四条规则照抄一遍(对,但跑不完)
// 洛谷 P1464 Function —— 第 ① 版:把题面上那四条规则照抄成代码(对,但跑不完)//// 输入:若干行 a b c,以 -1 -1 -1 结束// 输出:每行 "w(a, b, c) = ans"//// 这份为什么存在:这道题的递归**是题面直接给的**,一个字都不用自己想 ——// 所以它是「照着写就完事了吗」这个问题最干净的一次反例。//// ⚠ 它慢得非常夸张,而且题面自己都提醒了:「当 a,b,c 均为 15 时,调用的次数将非常的多」。// 实测(p1464Count.cpp 跑出来的):// w(8,8,8) 4 464 671 次// w(10,10,10) 489 048 226 次// w(12,12,12) 56 533 377 950 次// 而题目允许有 10^5 行询问。⇒ 一行都跑不完。//// ★ 顺带一个跑出来才看见的规律:**w(n,n,n) 正好是 2^n**(2、4、32、256、1024、4096)。// 知道这条不影响做题,但它是个提醒:**答案很小,不代表算出它很便宜。**
#include <bits/stdc++.h>using namespace std;
long long w(long long a, long long b, long long c) { if (a <= 0 || b <= 0 || c <= 0) return 1; // ★ 这一条必须排在最前面,见第 ③ 版 if (a > 20 || b > 20 || c > 20) return w(20, 20, 20); if (a < b && b < c) return w(a, b, c - 1) + w(a, b - 1, c - 1) - w(a, b - 1, c); return w(a - 1, b, c) + w(a - 1, b - 1, c) + w(a - 1, b, c - 1) - w(a - 1, b - 1, c - 1);}
int main() { long long a, b, c; while (cin >> a >> b >> c) { if (a == -1 && b == -1 && c == -1) break; cout << "w(" << a << ", " << b << ", " << c << ") = " << w(a, b, c) << "\n"; } return 0;}点「运行 ▶」看结果
实测(第 ⑦ 步那张表):
w(8,8,8) 4 464 671 次
w(10,10,10) 489 048 226 次
w(12,12,12) 56 533 377 950 次而题目允许 10^5 行询问。⇒ 一行都跑不完。
★ 顺带一个跑出来才看见的规律:w(n,n,n) 正好是 2^n(2、4、32、256、1024、4096)。
这条规律不影响做题,但它是个很重要的提醒:
答案很小,不代表算出它便宜。 w(10,10,10) 的答案只有 1024,却要走近五亿步。
4记忆化怎么加:先回答「这张表凭什么开得下」
第 ① 版慢在哪?不是规则复杂,是同一个 (a,b,c) 被反复算。
这句话可以直接量出来 —— 上面那份 p1464Trace.cpp 的第 ③ 段跑的就是这件事:
算 w(5,5,5) = 32 的过程里,一共调用了 5237 次 w(),
而不同的 (a,b,c) 只有 88 个。被重算最多的五个:
w(0,1,2) 被算了 647 遍
w(0,0,2) 被算了 437 遍
w(0,1,1) 被算了 391 遍
w(0,1,3) 被算了 362 遍
w(0,0,3) 被算了 362 遍
5237 次调用,只有 88 个不同的问题。 剩下的全是重复劳动 —— 把算过的答案记下来,下次直接取,就是记忆化。代码只多三行:
long long memo[21][21][21];
bool has[21][21][21]; // 这个格子算过没有
// …在四条规则判完、真正开算之前:
if (has[a][b][c]) return memo[a][b][c]; // 算过就直接拿
// …算完之后,存回去再返回:
has[a][b][c] = true;
return memo[a][b][c] = r;
题面里 a、b、c 的范围是 [-2⁶³, 2⁶³ - 1]。照这个范围开数组,多少台机器都放不下。
能开得下,是因为规则 1 和规则 2 自己把参数夹住了:
a 的取值 它会怎样 进不进 memo 表
---------------- ------------------------------ --------------------
a <= 0 规则 1:直接返回 1 不进(根本没往下走)
1 <= a <= 20 ★ 真正要记的就这一段 进
a > 20 规则 2:整个换成 w(20,20,20) 不进(换成别人了)⇒ 真正需要记的下标只有 1..20 这 20 个值,三个参数合起来
20 × 20 × 20 = 8000 格 —— 一个 21×21×21 的数组绰绰有余。
★ 这就是「该不该上记忆化」的判断方法,而且和答案有多大毫无关系:
一个子问题由哪几个量唯一决定?这几个量的组合有多少种? 组合数够小就上记忆化。
⚠ 顺带解释一个容易对不上的数:上面 w(5,5,5) 数到 88 个「不同的 (a,b,c)」,
而第 ⑦ 步那张表里 n = 5 的「真正算的格子」写的是 55。差的 33 个,
就是「有一个数 ≤ 0」的那些 —— 它们撞规则 1 直接返回,从不进表。
5第 ② 版:记忆化加对了,可答案错了
上一步那三行加完,速度立刻就够了 —— 可这道题的坑不在缓存上,在四条规则的顺序上。
题面把它写在明处了:
注意:例如
w(30, -1, 0)又满足条件 1 又满足条件 2,请按照最上面的条件来算,答案为 1。
这一版故意把两条判断调了个个儿:
if (a > 20 || b > 20 || c > 20) return w(20, 20, 20); // ⚠ 顺序反了
if (a <= 0 || b <= 0 || c <= 0) return 1;
// 洛谷 P1464 Function —— 第 ② 版:加了记忆化,快了,**但答案错了**//// 输入 / 输出:同第 ① 版//// 这份为什么存在:记忆化本身(解析页第 ④ 步)加对了,可**这道题的坑不在缓存上,// 在四条规则的顺序上** —— 这一版就是「速度已经够了、答案却错了」的样子。//// 题面把这个坑写在明处了:// 「注意:例如 w(30,-1,0) 又满足条件 1 又满足条件 2,请按照最上面的条件来算,答案为 1。」//// 而这一版故意把两条判断调了个个儿 —— 先看「有没有超过 20」,再看「有没有小于等于 0」://// if (a > 20 || b > 20 || c > 20) ... <- 错:它把 (30,-1,0) 抢走了// if (a <= 0 || b <= 0 || c <= 0) return 1;//// ⇒ w(30,-1,0) 会被当成 w(20,20,20) 来算,得到 1048576,而正确答案是 1。//// ★★ 这一版最值钱的一课和递归无关:// **当几条规则可能同时成立时,「先判哪一条」就是题目的一部分,不是实现细节。**// 题面特意举了个例子来说明顺序,那句话就是分数所在。//// ⚠ 而且这个错**很难自己发现**:只要你的测试数据里没有「又负又超 20」的组合,它就一直是对的。
#include <bits/stdc++.h>using namespace std;
long long memo[21][21][21];bool has[21][21][21];
long long w(long long a, long long b, long long c) { if (a > 20 || b > 20 || c > 20) return w(20, 20, 20); // ⚠ 顺序反了 if (a <= 0 || b <= 0 || c <= 0) return 1;
int x = (int)a, y = (int)b, z = (int)c; if (has[x][y][z]) return memo[x][y][z]; long long r; if (a < b && b < c) r = w(a, b, c - 1) + w(a, b - 1, c - 1) - w(a, b - 1, c); else r = w(a - 1, b, c) + w(a - 1, b - 1, c) + w(a - 1, b, c - 1) - w(a - 1, b - 1, c - 1); has[x][y][z] = true; return memo[x][y][z] = r;}
int main() { long long a, b, c; while (cin >> a >> b >> c) { if (a == -1 && b == -1 && c == -1) break; cout << "w(" << a << ", " << b << ", " << c << ") = " << w(a, b, c) << "\n"; } return 0;}点「运行 ▶」看结果
当几条规则可能同时成立时,「先判哪一条」就是题目的一部分,不是实现细节。
题面特意举了个例子来说明顺序 —— 凡是题面专门举例说明的地方,都是分数所在。
⚠ 而且这个错极难自己发现:只要你的测试数据里没有「又是负数、又超过 20」的组合, 它就一直是对的。样例正好没有这种组合。
⇒ 读题的时候,看到「又满足…又满足…请按照…」这种句子, 立刻回到代码里去数一遍 if 的顺序。
6第 ③ 版:顺序照题面来 + 记忆化 + 表跨询问保留(正解)
和前两版的差别一共三处,每一处对应一个真会挂人的地方:
① 加了记忆化 治第 ① 版的「跑不完」
② 规则顺序照题面来 治第 ② 版的「w(30,-1,0) 算错」
③ 缓存表跨询问保留 治一个前两版都看不出来的问题
题目最多有 10^5 行询问。如果每读一行就 memset 一次表,
等于每一行都从零开始重算 —— 记忆化白加。
而 w 是个纯函数:w(3,4,5) 是多少,跟前面问过什么毫无关系。
⇒ 表本来就该一直留着,问得越多越划算。
★ 一般化一句:缓存能不能跨询问复用,取决于「这个函数的答案会不会随外部状态变」。 纯函数就能。这个判断以后会一直用。
题面写着数值范围是 [-2⁶³, 2⁶³ - 1] ⇒ 必须用 long long 读。
用 int 读的话,一个 30000000000 会被悄悄截断成一个完全不同的数,
程序不报任何错,只是答案不对。
⚠ 但下标仍然是 int:clamp 到 20 之后最大就是 20,
别把数组开成 long long 索引 —— 那是另一种误解。
// 洛谷 P1464 Function —— 第 ③ 版:记忆化 + 规则顺序照题面来(正解)//// 输入:若干行 a b c,以 -1 -1 -1 结束(最多 10^5 行)// 输出:每行 "w(a, b, c) = ans",注意逗号后面有空格//// 和前两版的差别,一共就三处,每一处都对应一个真会挂人的地方://// ① 加了记忆化 —— 治第 ① 版的「跑不完」// ② 规则顺序照题面来 —— 治第 ② 版的「w(30,-1,0) 算错」// ③ 缓存表**跨询问保留** —— 治一个前两版都看不出来的问题:// 题目最多有 10^5 行询问,每行都清一次表的话,等于记忆化白加。// 而 w 是个纯函数,答案跟前面问过什么毫无关系 ⇒ 表本来就该一直留着。//// ⚠ 输入必须用 long long:题面写着数值范围是 [-2^63, 2^63-1]。// 用 int 读的话,一个 30000000000 会被截成一个完全不同的数,而程序不会有任何反应。// ⇒ 但**下标**仍然是 int(clamp 到 20 之后最大就是 20),别把数组开成 long long 索引。
#include <bits/stdc++.h>using namespace std;
long long memo[21][21][21];bool has[21][21][21]; // ★ 全局,跨询问保留
long long w(long long a, long long b, long long c) { if (a <= 0 || b <= 0 || c <= 0) return 1; // ★ 规则 1 必须排最前 if (a > 20 || b > 20 || c > 20) return w(20, 20, 20); // ★ 规则 2
int x = (int)a, y = (int)b, z = (int)c; if (has[x][y][z]) return memo[x][y][z];
long long r; if (a < b && b < c) r = w(a, b, c - 1) + w(a, b - 1, c - 1) - w(a, b - 1, c); else r = w(a - 1, b, c) + w(a - 1, b - 1, c) + w(a - 1, b, c - 1) - w(a - 1, b - 1, c - 1);
has[x][y][z] = true; return memo[x][y][z] = r;}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
long long a, b, c; while (cin >> a >> b >> c) { if (a == -1 && b == -1 && c == -1) break; cout << "w(" << a << ", " << b << ", " << c << ") = " << w(a, b, c) << "\n"; } return 0;}点「运行 ▶」看结果
7换一把可复现的尺子:加不加记忆化差多少
深入:这张表是怎么数出来的
// P1464 加不加记忆化,差多少 —— 换一把可复现的尺子//// 输入:无(表是写死的几组 (n,n,n))// 输出:一张表,对每组列出// 答案 w(n,n,n)// 1.没记忆化 调用了多少次 w()// 2.有记忆化 调用了多少次(含直接命中缓存的)// 真正算的格子 有多少个 (a,b,c) 被真的算了一遍//// ★ 两件事值得看:// ① **答案本身很小**(w(n,n,n) 正好是 2^n),可算出它要走的路却是天文数字 ——// 「答案小」和「好算」完全是两回事。// ② 最后一列**永远不超过 20×20×20 = 8000** —— 状态就三个量,每个不超过 20。// 这就是「该不该上记忆化」的全部判断依据。//// ⚠ 第 1 列到 n = 10 已经是四亿多次调用(这份程序因此要跑两三秒)。// 题面里点名的 w(15,15,15) 就不在表里了 —— 它跑不完。
#include <bits/stdc++.h>using namespace std;
long long naiveCalls = 0;long long naive(long long a, long long b, long long c) { naiveCalls++; if (a <= 0 || b <= 0 || c <= 0) return 1; if (a > 20 || b > 20 || c > 20) return naive(20, 20, 20); if (a < b && b < c) return naive(a, b, c - 1) + naive(a, b - 1, c - 1) - naive(a, b - 1, c); return naive(a - 1, b, c) + naive(a - 1, b - 1, c) + naive(a - 1, b, c - 1) - naive(a - 1, b - 1, c - 1);}
long long memo[21][21][21];bool has[21][21][21];long long memoCalls = 0, memoFilled = 0;
long long withMemo(long long a, long long b, long long c) { memoCalls++; if (a <= 0 || b <= 0 || c <= 0) return 1; if (a > 20 || b > 20 || c > 20) return withMemo(20, 20, 20); int x = (int)a, y = (int)b, z = (int)c; if (has[x][y][z]) return memo[x][y][z]; memoFilled++; long long r; if (a < b && b < c) r = withMemo(a, b, c - 1) + withMemo(a, b - 1, c - 1) - withMemo(a, b - 1, c); else r = withMemo(a - 1, b, c) + withMemo(a - 1, b - 1, c) + withMemo(a - 1, b, c - 1) - withMemo(a - 1, b - 1, c - 1); has[x][y][z] = true; return memo[x][y][z] = r;}
int main() { cout << " n 答案 w(n,n,n) 1.没记忆化 2.有记忆化 真正算的格子\n"; cout << "----- ------------- ------------ ---------- -----------\n";
for (int n : {1, 2, 5, 8, 10}) { naiveCalls = 0; long long ans = naive(n, n, n);
memset(has, 0, sizeof has); memoCalls = memoFilled = 0; withMemo(n, n, n);
cout << setw(5) << n << setw(16) << ans << setw(15) << naiveCalls << setw(13) << memoCalls << setw(14) << memoFilled << "\n"; }
cout << "\n答案那一列正好是 2^n —— 答案小,不代表算出它便宜。\n"; cout << "最后一列永远不超过 20*20*20 = 8000,因为状态就三个量、每个不超过 20。\n"; return 0;}点「运行 ▶」看结果
n 答案 w(n,n,n) 1.没记忆化 2.有记忆化 真正算的格子
----- ------------- ------------ ---------- -----------
1 2 5 5 1
2 4 21 21 5
5 32 5237 211 55
8 256 4464671 761 204
10 1024 489048226 1421 385- 答案那一列正好是
2^n—— 小得可怜,可算它要走的路是天文数字。 - 最后一列永远不超过
20 × 20 × 20 = 8000—— 就是第 ④ 步算过的那 8000 格, 状态就三个量、每个只有 20 种取值。 - 第 1 列和最后一列在
n = 10时差了 一百二十多万倍。
8回头看:这道题在教什么
- ★ 读不懂题面,就手算最小的一组。
w没有任何意义,硬读是读不懂的; 而把w(1,1,1)摊开成四项、看着它们全撞上规则 1,一次就够了。 - 题面给了递归,不等于题目简单。 这道题全部的分数在「跑得动 / 顺序对 / 读得对」上, 没有一件是算法。
- 题面专门举的例子,就是坑的位置。 看到「又满足…又满足…请按照…」, 立刻回去数 if 的顺序。
- 纯函数的缓存可以跨询问复用。 10^5 行询问时,这一条和记忆化本身一样重要。