题目描述
代码实现
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n, k;
while (cin >> n >> k)
{
int ans_1 = n - k;
vector<int> list(ans_1 + 1, 1);
for (int i = 2; i <= k; ++i)
{
for (int j = i; j <= ans_1; ++j)
{
list[j] += list[j - i];
}
}
cout << list[ans_1] << '\n';
int ans_2 = 1;
list.resize(n);//不要贪便宜把"n"改成"(n - 2)", 也不要图省事不重置容量, 小心吃到"1 1"之类的测试数据翻车
for (auto& i : list)
{
i = 1;
}
for (int i = 2, tmp = 3; tmp <= n;)
{
for (int j = i; j <= n - tmp; ++j)
{
list[j] += list[j - i];
}
ans_2 += list[n - tmp];
tmp += ++i;
}
cout << ans_2 << '\n';
int ans_3 = n % 2;
list.resize(n / 2);
for (auto& i : list)
{
i = 1;
}
for (int i = 2; i <= n; ++i)
{
int tmp = (n - i) / 2;
for (int j = i; j <= tmp; ++j)
{
list[j] += list[j - i];
}
if (!((n - i) % 2))//别脑子一晕乎就把"tmp"当成"(n - i)"用
{
ans_3 += list[tmp];
}
}
cout << ans_3 << '\n';
}
return 0;
}
思路讲解
如何只用一套递推方法、一个滚动数组求出3个看似截然不同的子问题呢?
一切都要从这题说起:
一个很基础的递归题, 但是其解题思想提供了这一动归题目的关键入口——把“数字”看成“苹果”。为了不让跨度过大,我们再来引入这一题:
这两题别的博客有比较详尽的解答,大家能体会到两者的共通之处就行了,我这里不多赘述。
只要理解了1664这一题,再能够将“放苹果”的思想推广至“放数字”,4117可谓是照抄即可。而有了“放数字”的思想之后,再回过头来看4119,便有了“这题也能放数字吗”的想法。
那这题究竟能否用这样的思路来解呢?答案是:当然可以。
先从第一个子问题开始:“求N划分成K个正整数之和的划分数目”。
乍一看跟1664中“把N个同样的苹果放在K个同样的盘子里”一模一样,但“不允许有盘子空着不放”却给人了一个下马威,看似这断开了两者的联系,却恰恰成了两者联系的关键所在。
如何把“不允许有盘子空着不放”转化为“允许有的盘子空着不放”呢?
这时我们来想一想1664中“f(i, k) = f(i, k-1) + f(i - k, k)”(总放法 = 有盘子为空的放法 + 没盘子为空的放法)的所谓“状态转移方程”。
正像其中“没盘子为空的放法”,我们提前在所有盘子里放一个,对于“不允许有盘子空着不放”的要求,后面再放不放也就无所谓了!
所以我们就这样把“求N划分成K个正整数之和的划分数目”变成了“把(N - K)个同样的苹果放在K个同样的盘子里,允许有的盘子空着不放”了,再将剩余思路照抄,根据该状态转移方程的性质优化为滚动数组即可。
那第二个子问题:“求N划分成若干个不同正整数之和的划分数目”呢?
好家伙,现在直接在“把N个同样的苹果放在若干个(反正超不了,就当作是N个吧)同样的盘子里,允许有的盘子空着不放”的后面加了一句“但不允许有两个盘子中的苹果数目相等”,这哪还有联系!?
嘿!为什么我们不想想让1664中解法得到的“数目可能相同”变成“一定不同”呢?
现在我们再回头来看看1664,解题的思路方法很妙,似乎解出来的方法已经按逆序排好了,
而且用“i - k”“铺地基”的思想第一次接触时也实在让人耳目一新……
等等,我前面是不是把“若干”直接当成了N?为什么要当成N呢,既然每个盘子里苹果数都不一样,它至少也是“{ 1, 2, 3, 4, …… }”的数量和排列吧?那再推下去,只要有一个数M,让1到M的数的和,也就是M * (M + 1) / 2,大于N就行了。
但谁又说这只能得到盘子的最大数量呢?既然1664能有逆序的感觉,我们把“{ 1, 2, 3, 4, …… }”反过来变成“{ ……, 4, 3, 2, 1 }”的逆序再“铺成地基”,这样相比之下把多的垫得更多,少的垫得更少,同样数量也因地基的数量差而变得不一样…这样每个盘子里苹果的数目不就“一定不同”了!
那要怎么实现呢?这个简单!重置一下滚动数组,照着原模原样重新滚一遍,在重新限定规模的同时给M个盘子垫上M * (M + 1) / 2个“地基”,再让每个“把(N - M * (M + 1) / 2)个同样的苹果放在M个同样的盘子里”的结果累加起来就是第二个子问题的答案了。
可惜这一题的子问题不只有两个,第三个便棘手不少:“求N划分成若干个奇正整数之和的划分数目”。
“所有盘子内的苹果数目必须为奇数”!?哈!这个我会,照之前的类比一下,顺着偶数垫“地基”找奇数组合就没问题了对吧?
可能是个方法,可惜我打的表看上去实在让人感觉麻烦……
偶数们分成的奇数组合在表里实在让人眼花缭乱,可真不像奇数们分成的组合不会只有偶数……
嘿!那我们为什么不去找偶数呢?只需在每个要用的“盘子”垫上1,与后面摞上的偶数自然就合为了奇数。
可是偶数组合要怎么找呢?光用脑想不如前两个子问题想的清楚,还是得打表:
这个表看上去可舒服多了,有了“垫1”的思想,也自然就看出5在这一子问题的解为(5, 0) + (4, 1) + (2, 3) + (3, 2) + (4, 1) + (5, 0) = 3,符合样例输出:
6个值相加,其中3个都是0,真恼人…让我们把奇数行去掉:
这下好多了,等等…这个表是不是看着有点熟悉…?
让我们看看1664的表,也就是前两个子问题的表:
是一致的!三个子问题其实共用了一个表!
啊哈!那这下就好解决了!状态转移方程是现成的,第三个子问题答案的累加方法也分析完了,代码实现也就水到渠成了!
至此,问题解决!
后记
非常感谢你能耐心看到这里。这只是一个准高中生在做题的时候想套用过往思路逃课,却误打误撞发现新大陆的过程。当然新大陆也让我这个想光速逃课的抓了3个小时脑壳……
代码中两处注释也是亲自踩到的坑,别跟我犯同样的错误,5ms的RE还是挺吓人的……
正如你所见,这个题既然三个子问题都可以用一套记忆秒,提前编程把表打完再挨个算3个子问题答案应该也可以,我这边想实践一下滚动数组的应用所以没用,有兴趣的读者可以试试,我还挺好奇那个优势更大或者各有什么优势之类的。
还有,听说第二个子问题和第三个子问题答案是一致的……
再次,感谢你的阅读。