文章目录
零崎的朋友很多Ⅰ(完全背包)
题目
零崎有很多朋友,其中有一个叫做lfj的接盘侠。
lfj是一个手残,他和零崎一起玩网游的时候不好好打本,天天看拍卖行,没过多久,就成为了一个出色的商人。时间一长,虽然挣了不少钱,却没时间练级了。
作为lfj的友人,零崎实在看不下去,于是他决定帮lfj一把。当然了,零崎肯定不会自己动手,活还得你们来干。
lfj可以提供给你们拍卖行所有能买到物品的价格和利润,由于游戏产出不限,所以可以假定只要有钱,即使是同一种东西,多少个也都能买到手。lfj还会告诉你他初始的成本。虽然零崎想让你们给出一次交易中利润最大的购买方案,但是lfj觉得只要知道最大利润就可以了。
分析
完全背包问题,每个物品无限多。
和0-1背包唯一的区别在于内层循环的顺序。下面分别分析为何一个是逆序的,一个是顺序的。
为什么0-1背包内循环是逆序的
网上有不少人在解释,我就不仔处心积虑构造例子了,简单讲点high level的。
首先需要明确,使用一维数组的本质是压缩了一个矩阵,原本的状态转移方程是
dp[i][v] = max{dp[i-1][v], dp[i-1][v-c[i]] + v[i]}。这里i表示研究的范围扩大到第i个物品,v表示容量。所以在矩阵里看,更新当前位置,靠的是这个位置左上方的值。现在这些值被压缩到了一个维度上,那么每个位置的值其实是这一列上最大的一个,这个值是以后要用的,那么如果是顺序的,前面的值覆盖了,后面的更新会误把同一行的新值当成左上方的值,所以不对。
为什么完全背包内循环是顺序的
上面所说的矩阵视角依然适用。但是现在,顺着走看起来会覆盖,但是其实要的就是覆盖,所谓覆盖就是这一行的值更新了这一行后面的值,而同一行表示同意个物品,所以这就相当于是这个物品被选取了多次。
核心是回归到矩阵,看一下每个值更新需要之前取得的那些值,这些值究竟能否在合适的时刻压缩到那个行向量上。
解答
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
int P,N;
int p[400];
int c[400];
int dp[20005];
while(scanf("%d%d", &P, &N) != EOF) {
for (int i = 0; i < N; i++) {
scanf("%d%d", &p[i], &c[i]);
}
memset(dp, 0, sizeof(dp));
for (int i = 0; i < N; i++) {
for (int j = c[i]; j <= P; j++) {
if (dp[j] < dp[j - c[i]] + p[i]) {
dp[j] = dp[j - c[i]] + p[i];
}
}
}
printf("%d\n", dp[P]);
}
return 0;
}
零崎的朋友很多Ⅱ(满0-1背包)
题目
零崎有很多朋友,其中有一个叫做lfj的接盘侠。
lfj是一个手残,他和零崎一起玩网游的时候不好好打本,天天看拍卖行,没过多久,就成为了一个出色的商人。不过再出色的投机商也有失手成为接盘侠的一天。所谓真正的接盘侠从来不给自己留活路。当lfj接盘成功之时,即分文不剩之日。
作为lfj的友人,零崎实在看不下去,于是他决定帮lfj一把。当然了,零崎肯定不会自己动手,活还得你们来干。
lfj可以提供给你们拍卖行所有能买到物品的价格和利润,还有他的本金。既然是接盘侠,就必须分文不剩。虽然零崎想让你们给出一次接盘中利润最大的购买方案,但是lfj觉得只要知道最大利润就可以了。
每组数据第一行为两个整数P和N,表示本金和拍卖行物品个数。(注意:每类物品只有一件
接下来N行,每行两个数据pi,ci代表第i类物品的利润和购买价格。
1<=P<=20000,1<=N<=300,1<=c,p<=200
分析
初始化的艺术。不同的背包初始化之后都是0,表示一个不装。但是这个问题,初始化只有容量为0的位置,其他位置用特殊值表示非法。这样的话从子问题开始,只有恰好装满的时候才有效,这样产生的大问题也只有装满才有效。
解答
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
int P,N;
int p[400];
int c[400];
int dp[20005];
while(scanf("%d%d", &P, &N) != EOF) {
for (int i = 0; i < N; i++) {
scanf("%d%d", &p[i], &c[i]);
}
memset(dp, -1, sizeof(dp));
dp[0] = 0;
for (int i = 0; i < N; i++) {
for (int j = P; j >= c[i]; j--) {
if (dp[j-c[i]]!=-1 && dp[j] < dp[j-c[i]] + p[i]) {
dp[j] = dp[j-c[i]] + p[i];
}
}
}
if (dp[P] == -1) {
printf("jpx\n");
} else {
printf("%d\n", dp[P]);
}
}
return 0;
}
零崎的朋友很多Ⅲ (区间dp)
题目
现在jhljx遇到了矩阵乘法,他当时就懵了。数都数不清的他,矩阵乘法怎么可能会算的清楚呢?虽然零崎觉得还不如让你们来算,不过好歹也要给jhljx个面子,给她留下一个证明自己数学实力的机会。为了减小jhljx的计算量,让他赶快算出不正确答案来(估计他算上几遍都不一定能出一个正确答案),零崎请你们帮助jhljx。
输入
多组输入数据。
每组数据以N开始,表示矩阵链的长度。接下来一行N+1个数表示矩阵的行/列数。
1<=N<=300
输出
对于每组样例,输出一行最少运算次数的方案,每两个矩阵相乘都用“()”括起来,详见样例
如果存在多种解决方案,最终输出结果选取先计算左边的矩阵,详见Hint
分析
经典的区间dp,注意更新的顺序问题。
此外这道题的输出很有意思,需要递归实现。当时居然想出来了,感动到流泪。。。
解答
#include <stdio.h>
#include <string.h>
void output(int s[][305], int l, int r) {
if (l == r) {
printf("A%d", l + 1);
return;
}
printf("(");
int cut = s[l][r];
output(s, l, cut-1);
output(s, cut, r);
printf(")");
}
int dp[305][305];
int s[305][305];
int main(int argc, char *argv[]) {
int N;
int val[400];
while(scanf("%d", &N) != EOF) {
for (int i = 0; i <= N; i++) {
scanf("%d", &val[i]);
}
memset(dp, -1, sizeof(dp));
memset(s, -1, sizeof(s));
for (int i = 0; i < N; i++) {
dp[i][i] = 0;
}
for (int i = N - 1; i >= 0; i--) {
for (int j = i + 1; j < N; j++) {
for (int k = i + 1; k <= j; k++) {
if (dp[i][j] == -1 || dp[i][j] >= dp[i][k-1] + dp[k][j] + val[i]*val[k]*val[j+1]) {
dp[i][j] = dp[i][k-1] + dp[k][j] + val[i]*val[k]*val[j+1];
s[i][j] = k;
}
}
}
}
output(s, 0, N-1);
//printf("%d\n", dp[0][N-1]);
printf("\n");
}
}
双“11”的抉择
题目
把钱花完了,所以单身了,单身了所以过双“11”,过双“11”所以把钱花完了。
今年Nova君(三号)照旧过着他暗无天日的“买买买”的双“11”,然而因为囊中羞涩,并不能够太任性。他的购物车中,列满了数不清的商品,共有N件,好多商品居然还不止一件 _(:3 」∠) 现在Nova君要做出一个艰难的抉择,他要从所有商品中挑出m件拼成一个订单,请问有多少种凑单的方法呢?求访法数对M的余数。
PS:同一种商品不作区分。
输入
多组测试数据(不超过100组)
每组数据两行,第一行为三个正整数N,m,M,具体意义详见描述,第二行为N个正整数a1,a2,an,代表第i个商品的个数
(1<=N,ai,m<=1000,2<=M<=10000)
输出
对于每组数据,输出一行,表示方法总数
分析
属于多重集合组合数问题,很像高考数学题,以为是个数学题,结果因为这道题里选取的物品个数、种类都在变,所以写不出组合数公式。
这个动态规划好难啊。。。。
状态分析
设dp[i][j] 为考虑前i个物品时,取j个的方案数。
当研究包含i个物品取j个的时候,方案可以分为两类。
第一类:不包含第i个物品,这类方案数为dp[i-1][j]
第二类:至少包含一个第i个物品,这类方案数如下(注意同种物品算一个,所以同一种物品挑多个的时候只会有一种方案。)
dp[i-1][j-1] + dp[i-1][j-2] + …+dp[i-1][0],加上第一类,总方法数为
dp[i][j] = dp[i-1][j] + dp[i-1][j-1] + dp[i-1][j-2] + …+dp[i-1][0]
看上去是三重循环?这样显然会超时啊。
但是如果观察形式特征,
dp[i][j-1] = dp[i-1][j-1] + dp[i-1][j-2] +…+dp[i-1][0] (以j<=a[i-1]为例)
所以dp[i][j]可以写成dp[i][j-1] + dp[i-1][j]
基于这样的思路,可以化简最后一层的时间复杂度
如果j<=a[i-1],如上所述dp[i][j] = ap[i][j-1] + dp[i-1][j]
反之j>a[i-1],dp[i][j-1] = dp[i-1][j-1] + dp[i-1][j-2] +…+dp[i-1][j-1-a[i-1]]
dp[i][j] =dp[i-1][j] + dp[i][j-1] - dp[i-1][j-1-a[i-1]] (减掉的这一项,是因为不能一直到0,所以把最后的项都去掉,注意我们前面的讨论里一直是拓展到0的)
解答
#include <stdio.h>
#define N 1010
int main(int argc, char * argv[]) {
int a[N];
int dp[N][N];
int n,m,M;
while(scanf("%d%d%d",&n,&m,&M) != EOF) {
for(int i = 0; i < n; i++) {
scanf("%d",&a[i]);
}
for(int i = 0; i <= n; i++) {
dp[i][0] = 1;
}
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
if(j -1 - a[i-1] >= 0) {
dp[i][j] = (dp[i-1][j] + dp[i][j-1] - dp[i-1][j-1-a[i-1]] +M)%M;
} else {
dp[i][j] = (dp[i-1][j] + dp[i][j-1])%M;
}
}
}
printf("%d\n",dp[n][m]);
}
}