注意以下三个问题,都需要求出满足要求的序列数量
- 给定一个数N,将其拆分为不重复的M以内的正整数
- 给定一个数N,将其拆分为M以内的正整数
- 给定一个数N,将其拆分为M以内的正整数组成的序列
举个例子,对于输入的N = 5, M = 3
- 问题1只接受 2, 3;
- 问题2接受
- (1,1,1,1,1)
- (1,1,1,2)
- (1,1,3)
- (1,2,2)
- (2,3)
- 问题三接受的就更多了,问题2中出现的任意序列的再排列都是问题三的一解,例如
- (1,1,1,2)
- (1,1,2,1)属于问题三的两种不同解
这三个问题都可以用0-1背包来解答,而且核心逻辑也非常相似
vector<int> dp(n + 1, 0)
dp[0] = 1;
// 问题1:给定一个数N,将其拆分为不重复的M以内的正整数
for (int i = 1; i <= M; ++i) {
for (int j = N; j >= i; --j) {
dp[j] += dp[j - i]; // dp[j] 表示和为 j 的方式数
}
}
// 问题2:给定一个数N,将其拆分为M以内的正整数
for (int i = 1; i <= M; ++i) {
for (int j = i; j <= N; ++j) {
dp[j] += dp[j - i]; // dp[j] 表示和为 j 的方式数
}
}
// 问题3:给定一个数N,将其拆分为M以内的正整数组成的序列
for (int i = 1; i <= N; ++i) {
for (int j = 1; j <= M; ++j) {
if(i >= j)
dp[i] += dp[j - i]; // dp[j] 表示和为 j 的方式数
}
}
看上去好像只是内外循环换了个次序,然后又是遍历顺序改变了一下,为什么可以做到三种不同的任务?
接下来我讲一下我个人的理解,如果不想理解的话大家记住这三类题目的板子,抱走就行
实际上,外循环中的元素在内循环中操作时被固定,内循环在操作dp时,dp中的继承方向与内循环遍历方向相同,则外循环元素会被多次采用,反之则只会被采用1次。
因为这是我用来说服自己的,所以看着确实有点拗口。咱们举个例子。
比如对于数字5, 我们要用2以内的数字(1,2)来表示它:
如果我们先遍历零数集合的可能性(1,2),也就是把M放外圈,那么我们现在先把1取出来,让内圈数字(1~5)用这个数字来构成自己。
那么对于每个dp(先做约束较弱的正序遍历,也就是可以重复使用数字)还记得我们的dp公式吗
我们假设我们已经知道了j - i这个数字的构成情况,那么在选取i这个数字就可以构成j了
dp[j] += dp[j - i]
所以我们有
0:()dp = 1
1:(1) dp = dp[0] + 0 = 1
2:(1,1)dp = dp[1] + 0 = 1
3:(1,1,1)dp = ... = 1
4:(1,1,1,1)dp = ... = 1
5:(1,1,1,1,1)dp = ... = 1
也就是说,当前一个数字使用了i后,我后一个数字还可以使用i来构成自己,那如果换成逆序呢?
5:()dp = 0
4:()dp = 0
3:()dp = 0
2:()dp = 0
1:(1) dp = dp[0] + 0 = 1
0:()dp = 1
这是为什么?简单来说,当5想要使用某个正整数元素x来构成自己的时候(这里是1),因为x > 0, 因此5 - x < 5 所以5 - x还没有被遍历到。遍历到是什么意思呢?就是指这个数字尝试了使用x来构成自己。
所以,当一个数j尝试使用某个x来构成自己时,如果其希望继承的数j op x如果还没被遍历到,那么就可以防止在构成中重复使用x。秘密并不在循环的升序或降序上,而在dp更新的方向上。
至此,我们解答了问题1和问题2的不同,那么内外循环的换序为什么可以从统计集合变为统计序列呢(当两个set互相拥有对方所有拥有的元素则相同,当array中对应元素相同才相同)我们继续使用上一个例子。当我们正序遍历完了1,我们获得了仅用1构成的序列
0:()dp = 1
1:(1) dp = dp[0] + 0 = 1
2:(1,1)dp = dp[1] + 0 = 1
3:(1,1,1)dp = ... = 1
4:(1,1,1,1)dp = ... = 1
5:(1,1,1,1,1)dp = ... = 1
外循环不会再被重复,因此我们再也不会碰到1作为零数的情况了。现在我们考虑在外层遍历和数,在内层遍历零数
现在我们尝试用(1,2)来构成1
0:()
1:(1)
看上去是不是没什么不一样?,不急,我们继续遍历2
0:()
1:(1)
内循环遍历1
2:(1,1)
内循环遍历2
2:(1,1)(2)
还是没太大差别,但是我们看到2的构成相比之前已经多出了一个状态,这会产生什么影响呢?我们继续遍历3
0:()
1:(1)
2:(1,1)(2)
内循环遍历1
3:(1,1,1)(2,1)在2的所有搭配后面append1
内循环遍历2
3:(1,1,1)(2,1)(1,2)在1的所有搭配后面append2
这下差别就明显了,由于每个和数都会尝试所有零数,而每个零数都曾在比当前和数小的时候被使用,因此就会有使用顺序的差别。
外循环不再有,内循环常停留。外循环遍历后,被遍历的元素状态就被锁死,不再会被修改。
这时候就有小可爱要问了,那根据排列组合,咱们是不是既能换遍历顺序,也能再换一次内外循环咧?
首先,如果换遍历顺序,只有换对和数的遍历才有意义,因为零数不管正反,它的状态不会记录在dp中。
而在外循环逆序遍历和数,那么目标数N一开始状态就被锁死在0了,因为我们假设dp[i - j]已经被计算好了,但逆序遍历的话当前dp[i - j]还没遍历到呢?
内层可以逆序的原因是当前内循环dp只记录是否使用当前外循环遍历到零数,而之前的零数状态已经确定了。