DP基础练习(4.21)
数塔
Description
在讲述DP算法的时候,一个经典的例子就是数塔问题,它是这样描述的:
有如下所示的数塔,要求从顶层走到底层,若每一步只能走到相邻的结点,则经过的结点的数字之和最大是多少?
Input
输入数据首先包括一个整数C,表示测试实例的个数,每个测试实例的第一行是一个整数N(1 <= N <= 100),表示数塔的高度,接下来用N行数字表示数塔,其中第i行有个i个整数,且所有的整数均在区间[0,99]内。
Output
对于每个测试实例,输出可能得到的最大和,每个实例的输出占一行。
Sample Input 1
1 5 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5
Sample Output 1
30
1 #include <cstdio> 2 #include <algorithm> 3 4 using namespace std; 5 6 int main() 7 { 8 int c; 9 scanf("%d",&c); 10 while(c--) 11 { 12 int a[101][101]={0}; 13 int dp[101][101]={0}; 14 int n; 15 scanf("%d",&n); 16 for(int i=1;i<=n;i++) 17 { 18 for(int j=1;j<=i;j++) 19 { 20 scanf("%d",&a[i][j]); 21 } 22 } 23 for(int i=1;i<=n;i++) 24 { 25 dp[n][i]=a[n][i]; 26 } 27 for(int i=n-1;i>=1;i--) 28 { 29 for(int j=1;j<=i;j++) 30 { 31 dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+a[i][j]; 32 } 33 } 34 printf("%d\n",dp[1][1]); 35 } 36 return 0; 37 }
音量调节
Description
一个吉他手准备参加一场演出。他不喜欢在演出时始终使用同一个音量,所以他决定每一首歌之前他都要改变一次音量。在演出开始之前,他已经做好了一个列表,里面写着在每首歌开始之前他想要改变的音量是多少。每一次改变音量,他可以选择调高也可以调低。
音量用一个整数描述。输入文件中给定整数beginLevel,代表吉他刚开始的音量,以及整数maxLevel,代表吉他的最大音量。音量不能小于0也不能大于maxLevel。输入文件中还给定了n个整数c1,c2,c3…..cn,表示在第i首歌开始之前吉他手想要改变的音量是多少。
吉他手想以最大的音量演奏最后一首歌,你的任务是找到这个最大音量是多少。
Input
第一行依次为三个整数:n, beginLevel, maxlevel。
第二行依次为n个整数:c1,c2,c3…..cn。
Output
输出演奏最后一首歌的最大音量。如果吉他手无法避免音量低于0或者高于maxLevel,输出-1。
Sample Input
5 3 7
Sample Output
HINT
0<=beginlevel<=maxlevel
Source
一开始便开始想用f[i]表示前i个物品能够得到的最大的音量,以为和装箱问题一样。于是借用一下强大的搜索引擎,发现一种叫布尔型dp,就是用f[i][j]表示前i个物品在音量为j时可行;记住这种表示方式;
1 #include <iostream> 2 #include <cstring> 3 #include <algorithm> 4 5 using namespace std; 6 7 int a[1010]; 8 bool dp[55][1010]; 9 10 int main() 11 { 12 int n,initial,maxl; 13 cin>>n>>initial>>maxl; 14 for(int i=1;i<=n;i++) 15 { 16 cin>>a[i]; 17 } 18 memset(dp,false,sizeof(dp)); 19 dp[0][initial]=true; 20 for(int i=1;i<=n;i++) 21 { 22 for(int j=0;j<=maxl;j++) 23 { 24 //dp[i][j]是第i次调节可以获得的音量j 25 dp[i][j]=((j+a[i]<=maxl)&&dp[i-1][j+a[i]])||((j-a[i]>=0)&&dp[i-1][j-a[i]]); 26 //j+a[i]<=maxl和 j-a[i]>=0判断是否超出dp[]的范围 27 //dp[i-1][j+a[i]]为真说明第i次调节后的j可以是第i-1次的某个音量减去a[i]得到 28 //dp[i-1][j-a[i]]为真说明第i次调节后的j可以是第i-1次的某个音量加上a[i]得到 29 } 30 } 31 for(int i=maxl;i>=0;i--) 32 { 33 if(dp[n][i]) 34 { 35 cout<<i<<endl; 36 return 0; 37 } 38 } 39 cout<<-1<<endl; 40 return 0; 41 }
消失之物
Description
ftiasch 有 N 个物品, 体积分别是 W1, W2, ..., WN。 由于她的疏忽, 第 i 个物品丢失了。 “要使用剩下的 N - 1 物品装满容积为 x 的背包,有几种方法呢?” -- 这是经典的问题了。她把答案记为 Count(i, x) ,想要得到所有1 <= i <= N, 1 <= x <= M的 Count(i, x) 表格。
Input
第1行:两个整数 N (1 ≤ N ≤ 2 × 103) 和 M (1 ≤ M ≤ 2 × 103),物品的数量和最大的容积。
第2行: N 个整数 W1, W2, ..., WN, 物品的体积。
Output
一个 N × M 的矩阵, Count(i, x)的末位数字。
Sample Input
3 2
1 1 2
Sample Output
11
11
21
HINT
如果物品3丢失的话,只有一种方法装满容量是2的背包,即选择物品1和物品2。
题解
先考虑不删除物品怎么做,其实就是一个背包,dp[i][j]=dp[i-1][j]+dp[i-1][j-w[i]];那么现在考虑少了物品i会减少几种方案,当x小于w[i]时,i物品一定不会被选上 g[i]=f[i] 当x大于等于w[i]时,i物品可能会被选上,直接求不选的情况比较困难。可以换个思路,设g[x]为不选当前物品的容量为x的方案数,用总方案数-选的方案数得到不选的方案数。总方案数及f[x],不选的方案数可以想为先不选i再最后把i选上,即g[x-w[i]]。
1 #include<cstdio> 2 #include<algorithm> 3 #include<cstring> 4 #include<iostream> 5 using namespace std; 6 const int N=100010; 7 int f[N],g[N],w[N]; 8 int n,m; 9 int main(){ 10 scanf("%d%d",&n,&m); 11 for(int i=1;i<=n;i++) scanf("%d",&w[i]); 12 13 f[0]=1; 14 for(int i=1;i<=n;i++){ 15 for(int j=m;j>=w[i];j--) 16 f[j]=(f[j]+f[j-w[i]])%10; 17 } 18 for(int i=1;i<=n;i++){ 19 for(int j=0;j<=m;j++){ 20 if(j<w[i]) g[j]=f[j]; 21 else g[j]=(f[j]-g[j-w[i]]+10)%10; 22 } 23 for(int j=1;j<=m;j++) printf("%d",g[j]); 24 printf("\n"); 25 } 26 return 0; 27 }
分析
很明显是个01背包的dp,但是问题在于统计这个答案上。感觉和容斥原理略有关系?于是研读了一下黄学长的极妙的做法
首先,f[j]表示装满容积为j的背包的方案数 很明显,f[j]+=f[j-w[i]] (f[0]=1)
统计答案我们用c[i][j]表示
再枚举I,j
如果f[j]<w[i],显然,装满容积j显然不可能用到第i个物品,所以c[i][j]=f[j]。
如果f[j]>=w[i],f[j]里就包括了选择了第i件物品的情况,那我们需要扣除掉这一部分。把扣除的这部分转换一下:
用第i件物品填满j ---> 用了其他物品填了j-w[i],所以c[i][j]=f[j]-c[i][j-w[i]]
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define N 2020 4 int n,m; 5 int f[N],w[N]; 6 int c[N][N]; 7 int main() 8 { 9 scanf("%d%d",&n,&m); 10 for(int i=1;i<=n;i++) 11 scanf("%d",&w[i]); 12 f[0]=1; 13 for(int i=1;i<=n;i++) 14 for(int j=m;j>=w[i];j--) 15 f[j]+=f[j-w[i]],f[j]%=10; 16 for(int i=1;i<=n;i++) 17 { 18 c[i][0]=1; 19 for(int j=1;j<=m;j++) 20 { 21 if(j>=w[i])c[i][j]=(f[j]-c[i][j-w[i]]+10)%10; 22 else c[i][j]=f[j]; 23 printf("%d",c[i][j]); 24 } 25 printf("\n"); 26 } 27 return 0; 28 }
货币系统
在网友的国度中共有 n 种不同面额的货币,第 i 种货币的面额为 a[i],你可以假设每一种货币都有无穷多张。为了方便,我们把货币种数为 n、面额数组为 a[1…n] 的货币系统记作 (n,a)。
在一个完善的货币系统中,每一个非负整数的金额 x 都应该可以被表示出,即对每一个非负整数 x,都存在 n 个非负整数 t[i] 满足 a[i]×t[i] 的和为 x。然而, 在网友的国度中,货币系统可能是不完善的,即可能存在金额 x 不能被该货币系统表示出。例如在货币系统 n=3, a=[2,5,9] 中,金额 1,3 就无法被表示出来。
两个货币系统 (n,a) 和 (m,b) 是等价的,当且仅当对于任意非负整数 x,它要么均可以被两个货币系统表出,要么不能被其中任何一个表出。
现在网友们打算简化一下货币系统。他们希望找到一个货币系统 (m,b),满足 (m,b) 与原来的货币系统 (n,a) 等价,且 m 尽可能的小。他们希望你来协助完成这个艰巨的任务:找到最小的 m。
输入
输入文件的第一行包含一个整数 T,表示数据的组数。
接下来按照如下格式分别给出 T 组数据。 每组数据的第一行包含一个正整数 n。接下来一行包含 n 个由空格隔开的正整数 a[i]。
输出
输出文件共有 T 行,对于每组数据,输出一行一个正整数,表示所有与 (n,a) 等价的货币系统 (m,b) 中,最小的 m。
输入
2 4 3 19 10 6 5 11 29 13 19 17
输出
2 5
说明
在第一组数据中,货币系统(2, [3,10])和给出的货币系统(n, a)等价,并可以验证不存在m < 2的等价的货币系统,因此答案为2。
在第二组数据中,可以验证不存在m < n的等价的货币系统,因此答案为5。
备注:
1 <= T <= 20, 1 <= n <= 100, 1 <= a[i] <= 25000
解题思路
题目说的很长,其实题目的大意就是有 n nn 个数,然后让你找 m mm 个数使得这 m mm 个数与 n nn 个数的表示范围相同。那么我们来分析一下,首先相同的数字肯定是没有意义的,我们对原数组进行去重,然后我们用一个数组 vis[i] vis[i]vis[i] 表示数字 i ii 是否能够表示,如果 vis[i]=true vis[i]=truevis[i]=true 可以表示,否则不可以表示。那么我们就对于 n nn 个数中的每一个数字进行判断,第一个 a[0] a[0]a[0] 肯定不能表示,那么我们结果 ans++ ans++ans++, 然后把 a[0] a[0]a[0] 能够表示的数字 i ii 全标记为 vis[i]=true vis[i]=truevis[i]=true;那么在检查第 i ii 个数字,如果能够用已经标记的数字表示,那么说明这个数可以用 之前的数表示,答案不记录;否则答案记录,而且将已经能够表示的数字加上 a[i] a[i]a[i],也进行标记 vis[j+a[i]]=true vis[j+a[i]]=truevis[j+a[i]]=true,直至循环完毕,输出 ans ansans 即可。
1 #include <iostream> 2 #include <stdio.h> 3 #include <string.h> 4 #include <stdlib.h> 5 #include <math.h> 6 #include <set> 7 #include <vector> 8 #include <algorithm> 9 #define pb push_back 10 #define mk make_pair 11 #define fi first 12 #define se second 13 using namespace std; 14 typedef pair<int, int> pii; 15 typedef long long LL; 16 typedef unsigned long long ULL; 17 const int MAXN = 25000+5; 18 int a[105]; 19 bool vis[MAXN]; 20 int main() { 21 int T; cin>>T; 22 while(T--) { 23 int n; cin>>n; 24 for(int i=0; i<n; i++) cin>>a[i]; 25 sort(a, a+n); 26 n = unique(a, a+n)-a; 27 memset(vis, false, sizeof vis); 28 vis[0] = true; 29 int ans = 0; 30 for(int i=0; i<n; i++) { 31 if(!vis[a[i]]) { 32 ans++; 33 for(int j=0; j<MAXN; j++) if(j+a[i]<=25000&&vis[j]) vis[j+a[i]] = true; 34 } 35 } 36 cout<<ans<<endl; 37 } 38 return 0; 39 }
题解
首先我想弱弱地说一句,比赛的时候我并没有看懂这道题,也就是说没有思路说多了都是泪
但是我下来想了一下,这道题可以用背包来做
递推式(转移方程)为f[i]=max(f[i],f[i−money[j]]+1)
f[i]表示i面值最多能被几张钱表示
f[i]=-inf表示有且只有它自己则f[i]=1
在开始之前初始化f[0]=0
就是这样~
1 #include<iostream> 2 #include<cstring> 3 using namespace std; 4 int a[500], n; 5 int ans; 6 int f[50000]; 7 int main() { 8 //freopen("money.in","r",stdin); 9 //freopen("money.out","w",stdout); 10 int T; 11 cin >> T; 12 for(int k = 1; k <= T; k++) { 13 memset(f, -10000, sizeof f); 14 ans = 0; 15 cin >> n; 16 for(int i=1; i<=n; i++) 17 cin >> a[i]; 18 f[0] = 0; 19 for(int i = 1; i <= n; i++) { 20 for(int j = a[i]; j <= 30000; j++) { 21 f[j] = max(f[j], f[j - a[i]] + 1); 22 } 23 } 24 for(int i = 1; i <= n; i++) 25 if(f[a[i]] == 1) { 26 ans++; 27 } 28 cout << ans << endl; 29 } 30 } 31
一个结论性的题目……吧。
首先手推一下,发现新货币系统中的最小值 m′ m'm
′
一定等于原来的货币系统中的最小值 m mm。如果大于,则新货币系统无法表达 m mm;如果小于,则原货币系统无法表达 m′ m'm
′
。
然后我们递归性地猜想:假如我去掉了这个最小值以及最小值能表达的数(因为新货币系统里面如果再有这些数就不够优秀了),那么再选择最小值是否也一定是最优的?
仿照上面的证明可以发现这个推论是正确的。
因此我们就可以得到我们的算法:
(1)找到原货币系统当前的最小值,加入新货币系统。
(2)在原货币系统中删除新货币系统能表达的数。
循环(1),(2)直到原货币系统没有任何数。
我们实现上可以不按这么写。我们可以从小到大枚举原货币系统中的数,判断它能否被新货币系统表达。能则跳过;不能则更新新货币系统。
判断以及更新可以用完全背包来做。
1 #include<cstdio> 2 #include<algorithm> 3 using namespace std; 4 const int MAXN = 100; 5 const int MAXM = 25000; 6 bool dp[MAXM + 5]; 7 int a[MAXN + 5]; 8 void solve() { 9 int n, m = 0, lim = 0; 10 scanf("%d", &n); 11 for(int i=1;i<=n;i++) { 12 scanf("%d", &a[i]); 13 lim = max(lim, a[i]); 14 } 15 for(int i=0;i<=lim;i++) 16 dp[i] = false; 17 sort(a+1, a+n+1); dp[0] = true; 18 for(int i=1;i<=n;i++) { 19 if( !dp[a[i]] ) { 20 for(int j=a[i];j<=lim;j++) 21 dp[j] |= dp[j-a[i]]; 22 m++; 23 } 24 } 25 printf("%d\n", m); 26 } 27 int main() { 28 int T; 29 scanf("%d", &T); 30 for(int i=1;i<=T;i++) 31 solve(); 32 return 0; 33 }