https://www.cnblogs.com/violet-acmer/p/9852294.html
题解:
思路一:完全背包转“01”背包
考虑到第ki个怪最多杀min(m/b[ki],s)个,于是可以把第ki个怪转化为min(m/b[ki],s)个忍耐度及经验值均不变的怪,然后求解这个01背包问题。
(1):不用滚动数组优化
本题有三个限制条件①怪物种类②忍耐度③杀怪数。
如果不使用滚动数组优化空间,则需要开个三维数组dp[ maxMaster ][ max_m ][ max_s ]。
dp[ tot ][ i ][ j ]的含义是杀第tot个怪时,耗费 i 个忍耐度和 j 个杀怪数所获得的最大经验值。
1 void Solve() 2 { 3 int tot=0;//把所有的 ki 怪转化为min(s,m/b[ki])个忍耐度及经验值均不变的物品时的总个数 4 for(int kind=1;kind <= k;++kind) 5 { 6 int x=min(s,m/b[kind]);//第 ki 个怪最多可转化成 x 个 7 while(x--)//将这 x 依次加入到背包中 8 { 9 for(int i=1;i <= m;++i)//当前耗费的忍耐度 10 for(int j=1;j <= s;++j)//当前杀怪数 11 if(i >= b[kind]) 12 dp[tot][i][j]=max(dp[tot-1][i][j],dp[tot-1][i-b[kind]][j-1]+a[kind]); 13 else 14 dp[tot][i][j]=dp[tot-1][i][j]; 15 tot++; 16 } 17 } 18 }
思路完全正解,提交试试,返回的结果竟然是MLE...............
(2):使用滚动数组优化
既然MLE,那我用滚动数组优化一下总行了吧
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 using namespace std; 5 #define mem(a,b) memset(a,b,sizeof(a)) 6 const int maxn=100+50; 7 8 int n,m,k,s; 9 int a[maxn],b[maxn]; 10 int dp[maxn][maxn]; 11 12 int Solve() 13 { 14 mem(dp,0); 15 bool index=1; 16 for(int kind=1;kind <= k;++kind) 17 { 18 int x=min(m/b[kind],s); 19 while(x--)//x 个 ki 怪物 20 { 21 for(int i=m;i >= b[kind];--i) 22 for(int j=1;j <= s;++j) 23 dp[i][j]=max(dp[i][j],dp[i-b[kind]][j-1]+a[kind]); 24 } 25 } 26 int res=m+1; 27 for(int i=1;i <= m;++i) 28 for(int j=1;j <= s;++j) 29 if(dp[i][j] >= n) 30 res=(res > i ? i:res);//找到经验值达到n以上的最小的忍耐度 31 return m-res; 32 } 33 34 int main() 35 { 36 while(~scanf("%d%d%d%d",&n,&m,&k,&s)) 37 { 38 for(int i=1;i <= k;++i) 39 scanf("%d%d",a+i,b+i); 40 printf("%d ",Solve()); 41 } 42 }
bingo,正解,不过,来分析一下此种做法的时间复杂度。
对于最坏的情况,m=100,k=100,s=100,且对于所有的 i 有 a[i] = b[i] =1,其时间复杂度高达O(n^4),要不是此题范围小,指定超时。
那么,还有比这更有的算法吗?
有个稍加优化的方法,可以将最坏的时间复杂度变为O(n^3log(n))。
把第ki个怪拆成忍耐度为b[ki]*(2^x)、经验值为a[ki]*(2^x)的若干个怪,其中 x 满足 b[ki]*(2^x) < m && (2^x) < s 。
这是二进制的思想,因为不管最优策略杀几头第 ki 个物品,总可以表示成若干个 2^x 个怪物的和。
这样把每头怪拆成O( log(min(m/b[kind],s)) )头怪,是一个很大的改进。
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cmath> 5 using namespace std; 6 #define mem(a,b) memset(a,b,sizeof(a)) 7 const int maxn=100+50; 8 9 int n,m,k,s; 10 int a[maxn],b[maxn]; 11 int dp[maxn][maxn]; 12 13 int Solve() 14 { 15 mem(dp,0); 16 bool index=1; 17 for(int kind=1;kind <= k;++kind) 18 { 19 int x=log(min(m/b[kind],s))/log(2); 20 for(int tot=0;tot <= x;++tot) 21 { 22 for(int i=m;i >= (1<<tot)*b[kind];--i) 23 for(int j=(1<<tot);j <= s;++j) 24 dp[i][j]=max(dp[i][j],dp[i-(1<<tot)*b[kind]][j-(1<<tot)]+(1<<tot)*a[kind]); 25 } 26 } 27 int res=m+1; 28 for(int i=1;i <= m;++i) 29 for(int j=1;j <= s;++j) 30 if(dp[i][j] >= n) 31 res=(res > i ? i:res);//找到经验值达到n以上的最小的忍耐度 32 return m-res; 33 } 34 35 int main() 36 { 37 while(~scanf("%d%d%d%d",&n,&m,&k,&s)) 38 { 39 for(int i=1;i <= k;++i) 40 scanf("%d%d",a+i,b+i); 41 printf("%d ",Solve()); 42 } 43 }
思路二:完全背包+滚动数组优化空间
设dp[i][j]表示消耗 i 个忍耐度,杀 j 头怪所获得的最大经验值。
状态转移方程:
dp[i][j]=max(dp[i][j],dp[i-b[k1]][j-1]+a[k1])
dp[i-b[k1]][j-1]+a[k1] : 杀k1怪所获得最大经验值
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 using namespace std; 5 #define mem(a,b) memset(a,b,sizeof(a)) 6 const int maxn=100+50; 7 8 int n,m,k,s; 9 int a[maxn],b[maxn]; 10 int dp[maxn][maxn];//dp[i][j] : 所需耐力值为i杀怪数为j时所获得的最大经验值 11 12 int Solve() 13 { 14 mem(dp,0); 15 for(int k1=1;k1 <= k;++k1) 16 for(int i=b[k1];i <= m;++i) 17 for(int j=1;j <= s;++j) 18 dp[i][j]=max(dp[i][j],dp[i-b[k1]][j-1]+a[k1]); 19 for(int i=1;i <= m;++i) 20 for(int j=1;j <= s;++j) 21 if(dp[i][j] >= n) 22 return m-i; 23 return -1; 24 } 25 int main() 26 { 27 while(scanf("%d%d%d%d",&n,&m,&k,&s) != EOF) 28 { 29 for(int i=1;i <= k;++i) 30 scanf("%d%d",a+i,b+i); 31 printf("%d ",Solve()); 32 } 33 return 0; 34 }
总结:
这种题设dp变量很重要,要设成几维的以及含义。
设成几维的?
有多少个限制条件,就设置成几维的,例如此题有三个限制条件①怪物种类②忍耐度③杀怪数
如果不适用滚动数组,则需要设置成三维数组。
如果使用滚动数组优化空间,则把第一个限制条件开辟的空间省去了,但第一个限制条件要在最外层循环处