小小的dp魔法

注意以下三个问题,都需要求出满足要求的序列数量

  • 给定一个数N,将其拆分为不重复的M以内的正整数
  • 给定一个数N,将其拆分为M以内的正整数
  • 给定一个数N,将其拆分为M以内的正整数组成的序列

举个例子,对于输入的N = 5, M = 3

  1. 问题1只接受 2, 3;
  2. 问题2接受
    1. (1,1,1,1,1)
    2. (1,1,1,2)
    3. (1,1,3)
    4. (1,2,2)
    5. (2,3)
  3. 问题三接受的就更多了,问题2中出现的任意序列的再排列都是问题三的一解,例如
    1. (1,1,1,2)
    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只记录是否使用当前外循环遍历到零数,而之前的零数状态已经确定了。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值