九 复杂DP 1.例题 1.1 acwing.1050. 鸣人的影分身 《信息学奥赛一本通》
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 在火影忍者的世界里,令敌人捉摸不透是非常关键的。 我们的主角漩涡鸣人所拥有的一个招数——多重影分身之术——就是一个很好的例子。 影分身是由鸣人身体的查克拉能量制造的,使用的查克拉越多,制造出的影分身越强。 针对不同的作战情况,鸣人可以选择制造出各种强度的影分身,有的用来佯攻,有的用来发起致命一击。 那么问题来了,假设鸣人的查克拉能量为 M,他影分身的个数最多为 N,那么制造影分身时有多少种不同的分配方法? 注意: 影分身可以分配0 点能量。 分配方案不考虑顺序,例如:M=7 ,N=3 ,那么 (2 ,2 ,3 ) 和 (2 ,3 ,2 ) 被视为同一种方案。 输入格式 第一行是测试数据的数目 t。 以下每行均包含二个整数 M 和 N,以空格分开。 输出格式 对输入的每组数据 M 和 N,用一行输出分配的方法数。 数据范围 0 ≤t≤20 ,1 ≤M,N≤10 输入样例: 1 7 3 输出样例: 8
思路:
整数划分问题 ,经典问题,积累。
算法1:递归dfs。(20ms)
这道题目相当于是把n个苹果放m个盘子里的一道题。(比较经典的题目)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <cstdio> #include <algorithm> #include <iostream> using namespace std ;int f (int x,int y) { if (x == 0 ) return 1 ; if (y == 0 ) return 0 ; if (y > x){ return f(x,x); } return f(x - y, y) + f(x, y - 1 ); } int main () { int t,n,m; cin >> t; while (t --){ cin >> n >> m; cout << f(n,m) << endl ; } return 0 ; } 作者:白線流つ 链接:https: 来源:AcWing 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
算法2:dp。(20ms)
实际上我们可以发现,在递归的过程中就是要用到之前的数据,继而这道题可以转换为记忆化搜索将结果保存来做,即dp做法,但是这个dp是从递归去思考出来的- -而不是像灿总那样直接思考dp做法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <iostream> #include <cstdio> using namespace std ;int a[25 ][25 ],m,n;int main () { int t,m,n; for (m=0 ;m<=10 ;m++) { for (n=0 ;n<=10 ;n++) { if (m<n)a[m][n]=a[m][m]; else if (m==0 )a[m][n]=1 ; else if (n==0 )a[m][n]=0 ; else a[m][n]=a[m-n][n]+a[m][n-1 ]; } } scanf ("%d" ,&t); for (int i=1 ;i<=t;i++) { scanf ("%d%d" ,&m,&n); printf ("%d\n" ,a[m][n]); } return 0 ; }
算法3:爆搜。(Java782ms,C++22ms)
暴力枚举每个盒子放多少个球,为了方便从左到右的球的数量从小到大递增,dfs过程中需要添加多start作为开始枚举的位置。
枚举时注意方案是不考虑顺序的!所以球的数量从小到大递增。可以画递归搜索树帮助理解!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import java.util.Scanner;public class Main { static int n; static int m; static int ans = 0 ; static void dfs (int u,int nums,int start) { if (u == n + 1 ) { if (nums == 0 ) { ans ++; } return ; } if (start > nums) return ; for (int i = start ;i <= nums;i ++) { dfs(u + 1 ,nums - i,i); } } public static void main (String[] args) { Scanner scan = new Scanner(System.in); int T = scan.nextInt(); while (T -- > 0 ) { ans = 0 ; m = scan.nextInt(); n = scan.nextInt(); dfs(1 ,m,0 ); System.out.println(ans); } } } 作者:小呆呆 链接:https: 来源:AcWing 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
算法4:y总DP分析。(11ms)
这里的集合划分依据比较难想到,但思路和前面2种算法有联系。
最小值=0:f[i,j]表示的所有方案中都空出至少1个盘子,转化为f[i,j-1];
最小值>0:f[i,j]表示的所有方案中每个盘子至少有1个苹果,就可以让所有盘子的苹果数-1,是等价的,转化
为f[i-j,j]。
注意初始化f[0,0]为1而不是0。状态计算满足的条件是j>=1和i>=j。 DP问题注意细节!!!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <iostream> #include <cstring> using namespace std ;int m,n;int dp[11 ][11 ];int main () { int t; cin >> t; while (t--){ cin >> m >> n; for (int i = 0 ;i <= n;i++) dp[0 ][i] = 1 ; for (int i = 1 ;i <= m;i++) for (int j = 1 ;j <= n;j++){ dp[i][j] = dp[i][j-1 ]; if (i >= j) dp[i][j] += dp[i-j][j]; } cout << dp[m][n] << endl ; } return 0 ; }
1.2 acwing.1047. 糖果 《信息学奥赛一本通》
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 由于在维护世界和平的事务中做出巨大贡献,Dzx被赠予糖果公司2010 年5 月23 日当天无限量糖果免费优惠券。 在这一天,Dzx可以从糖果公司的 N 件产品中任意选择若干件带回家享用。 糖果公司的 N 件产品每件都包含数量不同的糖果。 Dzx希望他选择的产品包含的糖果总数是 K 的整数倍,这样他才能平均地将糖果分给帮助他维护世界和平的伙伴们。 当然,在满足这一条件的基础上,糖果总数越多越好。 Dzx最多能带走多少糖果呢? 注意:Dzx只能将糖果公司的产品整件带走。 输入格式 第一行包含两个整数 N 和 K。 以下 N 行每行 1 个整数,表示糖果公司该件产品中包含的糖果数目,不超过 1000000 。 输出格式 符合要求的最多能达到的糖果总数,如果不能达到 K 的倍数这一要求,输出 0 。 数据范围 1 ≤N≤100 ,1 ≤K≤100 ,输入样例: 5 7 1 2 3 4 5 输出样例: 14 样例解释 Dzx的选择是2 +3 +4 +5 =14 ,这样糖果总数是7 的倍数,并且是总数最多的选择。
思路:
考察背包问题的应用,本题考察01背包问题。背包问题也就是一种选择模型。
DP数组的第1维度往往是数量,第2维度往往是限制条件。
自己想不到DP集合的状态表示内容。这里状态表示的是方案,不是方案数量,属性是糖果总数的最大值。
筛选标准:最后不同的一步 (在这里是选不选这个物品)
不选的话:子集合都不包括第 $i$ 个物品, 表示的意义就是前 $i-1$ 个物品, $\% k=j$ 的集合: $d p(i-1, j)$ 。
选择第 $i$ 个物品:子集合都包括第 $i$ 个物品, 并且前面选择的商品和 $S$ 满足 $(S+a[i]) \% k=j$, 而我们需要从和这个 $S$ 有关的集合, 状态记录的都是% $k$ 后的结果, 所以我们只需要知道 $S \% k(=(j-a[i]) \% k)$ 的那个状态的值。
由于第2维度不具有单调性,所以不能优化成1维,其实01背包问题大多数情况下也没必要优化,一般空间大小都够。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <cstdio> #include <cstring> #include <algorithm> using namespace std ;const int N = 105 ;int dp[N][N];int main () { int n,k; scanf ("%d%d" ,&n,&k); memset (dp,-0x3f ,sizeof dp); dp[0 ][0 ] = 0 ; int w; for (int i = 1 ;i <= n;i++){ scanf ("%d" ,&w); for (int j = 0 ;j < k;j++) dp[i][j] = max(dp[i-1 ][j],dp[i-1 ][(j+k-w%k)%k] + w); } printf ("%d\n" ,dp[n][0 ]); return 0 ; }
设置memset(f,-0x3f,sizeof f);
的解释,因为memset是按照字节赋值的。一个整型int有四个字节,所以这样赋值后,每个数组元素的值都变成了 -0x3f3f3f3f,而这个数的大小在-1e9的左右,由于一般的数据都不会小于-1e9,所以这时候它可以作为无穷小来用。
初始化:$d p(0,0)=0, d p(0, i)(i \neq 0)$ 都是不合法的状态 , 所以必须要初始化为负无穷 ,不然的话可以选择先初始化第一件物品的所有情况,再迭代第二个物品之后(仔细想想, 如果不合法的状态初始化为0了, 后面递推的时候, 答案就会变大)
关于负余数的说明:下面大致说明下正确性, 1.假如 $(j-w[i]>=0)$, 由于 $k \% k=0$, 所以不会影响。 2.假如 $(j-w[i]<0)$, 带个负数进去试下就可以验证,在这里 $+k$ 可以使 $(j-w[i]) \% k$ 变成 $[0, n-1]$之间,再取余仍然是 $[0, n-1]$ 之间。