POJ 4119 复杂的整数划分问题 一个滚动数组4ms全部搞定!

题目描述

 OpenJudge - 4119:复杂的整数划分问题

 

代码实现


#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个看似截然不同的子问题呢?

        一切都要从这题说起:

OpenJudge - 1664:放苹果

        一个很基础的递归题, 但是其解题思想提供了这一动归题目的关键入口——把“数字”看成“苹果”。为了不让跨度过大,我们再来引入这一题:

OpenJudge - 4117:简单的整数划分问题

        这两题别的博客有比较详尽的解答,大家能体会到两者的共通之处就行了,我这里不多赘述。

        只要理解了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个子问题答案应该也可以,我这边想实践一下滚动数组的应用所以没用,有兴趣的读者可以试试,我还挺好奇那个优势更大或者各有什么优势之类的。

还有,听说第二个子问题和第三个子问题答案是一致的……

再次,感谢你的阅读

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值