题目清单
背包
P1734 最大约数和
这道题当时看到,立马想起来之前写的一题整数拆分(在这篇博客里计数类DP),就是得出一个数加法组合方案的个数比如4=1+1+1+1=3+1=2+2=4一共四种
既然整数拆分能够得出一个数所有加法组合方案,就想着用这个思路求一个数最大约数和,就有了下面的代码,但其实两个题目又很不一样
如果像整除拆分那道题这样写:
for (int j = 1; j <= S; j++)
for (int k = 0; k * i <= j; k++)
就有很多数重复了,重复计算,没有意义.比如4和2*2,所以把for(k)这一行去掉修改一下就行
第一种代码的f[i][j]都表示处理完前i个正整数(i是s加法组成方案里的一个数),总大小为j的最大约数和。
第二种代码f[i]表示大小为i的最大约数和
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e3 + 5;
int s[N], f[N][N];
void Inti()
//初始化所有正整数的约数和,这个预处理和朴素筛法类似,通过每一个数的倍数把该倍数的约数里加上这个数
//用质数筛优化更省时间
{
for (int i = 1; i <= N/2; i++) {
//s[i]+=i;题目说一个数的约数和不包括这个数自身,所以对于i来说i不用加入i的约数和
for (int j = i + i; j <= N; j += i)
s[j] += i;
}
}
int main()
{
Inti();
int S;
cin >> S;
for (int i = 1; i <= S; i++)
for (int j = 1; j <= S; j++)
for (int k = 0; k * i <= j; k++)
f[i][j] = max(f[i][j], f[i - 1][j - k * i] + s[k * i]);
cout << f[S][S];
return 0;
}
变成这样,从145ms->48ms
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e3 + 5;
int s[N], f[N];
void Inti()//初始化所有正整数的约数和
{
for (int i = 1; i <= N/2; i++)
for (int j = i + i; j <= N; j += i)
s[j] += i;
}
int main()
{
Inti();
int S;
cin >> S;
for (int i = 1; i <= S; i++)
for (int j = 1; j <= i; j++)
//j是i加法构成中的一个部分数,比如3=2+1的2
f[i] = max(f[i], f[i - j] + s[j]);
//f[i]表示处理完i所以部份数的最大约数和
cout << f[S];
return 0;
}
想着整数划分那道题可不可以用上面这个代码写发现不行,然后发现这两题其实差别很大,因为上面取的是最大值,而整数划分这题是在上一层的体积的基础上加上,并且2+2!=4(4=2+2=3+1=4),但是本题求的最大值,如果用那个含k的代码s[2]+[2]<s[4]没啥影响。
#include<iostream>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int f[N];
int main()
{
int n;
cin >> n;
f[0] = 1;//初始化
//i表示处理完i个元素,j表示当前背包体积,k表示选第i元素的个数
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)//j从0开始
f[i] = (f[i] + f[i - j]) % mod;
cout << f[n];
return 0;
}
也可以把循环改成这样
这样就是把题目转换成01背包问题(我觉得这个思路最好,因为求得是小于等于S的最大约数和,那就用1~S这些数用来装填背包,不一定要装的满满的,可以有空,然后每个数的约数和已经预处理过了,就作为每个数的价值,那就是体积一定的背包最多能装价值为多大的物品)
for (int i = 1; i <= S; i++)
for (int j = S; j >= i; j--)
f[j] = max(f[j], f[j - i]+s[i]);
为什么我一开始没看出来是01背包我是不是个傻子啊啊
为什么是01背包而不是完全背包,比如1+1+1+1=4,这四个数的约数和为0,而4的约数和为3,几个相同的数的累加的约数和一定大于等于这个数各约数和的累加。
a+a+…+a=k* a,k* a除了拥有a的约数还有k的约数还有a* k的余数,(a* k的约数)* (k的约数)* (a的约数), 而k个(a的约数)之和就是k* (a的约数)。a* k的约数大于等于k。
多重背包应该也能写,但没必要。
完全背包
P1964 【mc生存】卖东西
多重背包写法
#include<iostream>
#include<map>
#include<algorithm>
using namespace std;
const int N = 1e6;
int f[N];
struct data {
int a, b, c;
};
int possess(int k, int maxv) {//k件物品占格数
int sum = 0;
sum = k / maxv;
if (k % maxv) sum++;
return sum;
}
int main()
{
int m, n;
cin >> m >> n;
m = 21 - m;
map<string,struct data>M;//物品名称,物品相关数据
for (int i = 0; i < n; i++) {
string name;
int a, b, c;
cin >> a >> b >> c >> name;
M[name].a += a;
M[name].b = b;
M[name].c = c;
}
//多重背包写法
int N = M.size();
for (auto x : M) {
int v = (x.second).a;
int maxv = (x.second).c;
int p = (x.second).b;//单件物品价格
for (int j = m; j >= 0; j--)
//开始我是写for (int j = 0; j <= 0; j++),wa了,测试发现当格子不被用完时有问题,调试一下就会发现答案是128,64的两倍
//测试样例:19 3 | 63 1 64 a | 1 1 64 a
for (int k = 0; k <= v; k++) {
if (k == 64) {
int p = 1;
}
int g = possess(k, maxv);
if (g > j) break;
f[j] = max(f[j], f[j - g] + k * p);
}
}
cout << f[m];
return 0;
}
贪心
把同种物品归纳结束后按照每个物品一个能占几件将物品按格子进行打包,接着排序,用一格子价值最大的打包物件填充背包里剩余格子数
#include<iostream>
#include<map>
#include<algorithm>
using namespace std;
const int N = 1e6;
int w[N];//体积为一格的价格
struct data {
int a, b, c;
};
bool cmp(int a, int b) {
return a > b;
}
int main()
{
int m, n;
cin >> m >> n;
int V = 21 - m, sum = 0;
map<string,struct data>M;//物品名称,物品信息
for (int i = 0; i < n; i++) {
string name;
int a, b, c;
cin >> a >> b >> c >> name;
M[name].a += a;
M[name].b = b;
M[name].c = c;
}
int pos = 0;
for (auto x : M) {
int maxv = (x.second).c, p = (x.second).b;
int v = (x.second).a;
while (v >= maxv) {
w[++pos] = maxv * p;
v -= maxv;
}
if (v) w[++pos] = v * p;
}
sort(w + 1, w + pos + 1,cmp);
for (int i = 1; i <= V; i++)
sum += w[i];
cout << sum;
return 0;
}
线性DP
P3009 [USACO11JAN(奶牛们的最大收益)
题目链接–P3009 [USACO11JAN]Profits S–最大子序列和
这题本来想用前缀和写的,大概算一下超时,本着侥幸的想法试了一下果然WA了
题目本身就是求最大子序列和,定义sum存储当前子序列和,当sum<0时置空sum=0,因我们设sum<0代表的一段子序列为a,接着的一段子序列为b,a+b<b这是恒成立的,所有max(最长子序列)只会存b而不会存a+b(这个思路一开始有想到,但太久没写这种题竟然一时没想通,离谱)
这里要注意的一个地方就是如果每一个子元素都小于0,那么最长子序列取得时这些元素最大的一个,要先maxs=max(sum,maxs),然后才可以if(sum<0) sum=0.
这个刚开始没意识到又WA了两发
#include<iostream>
using namespace std;
int main()
{
int maxs = -0x3f3f3f3f, sum = 0;
int n;
cin >> n;
for (int i = 0; i < n; i++) {
int k;
cin >> k;
sum += k;
maxs = max(maxs, sum);
if (sum < 0) sum = 0;
}
cout << maxs;
return 0;
}
搜索
P1044 [NOIP2003 普及组]栈
看到题,没思路,我就是那一只最菜的菜狗
看了一下题解,有位dalao一口气来了四种做法,我直呼🐂🍺并且ORZ%%
挑了两个方法简单说一下:
方法一:卡特兰数
这道题其实就是一个栈(无穷大)的进栈序列为1,2,3,…,n,有多少个不同的出栈序列?
准备入栈的序列为1.2…k.k+1…n-1.n。n个数,我们记最后一个出栈的数为k,因为k最后一个出栈,所以当k位于栈底的时候,k前面的数已经全部出栈共k-1个数。记n个数出栈的方案为f[n],那么前k-1个数出栈的方案就是f[k-1],既然k最后一个出栈,那k就一直待在栈底,所以剩下的n-k个数也按一定顺序出栈,然后这个k数可以是1-n中的任意一个。
所以for(int k=1;k<=n;k++) f[i]+=f[k-1]*f[n-k].
到这里我真的没看出来和卡特兰数有什么关系…我的问题,因为当时第一次接触卡特兰数给的公式是f(n)=C(2n,n)-C(2n,n-1),解决的是01序列问题。
—这里可以参考我之前写的一篇博客《高斯消元+求组合数+卡特兰数》
然后我去百度查了一下,在百度百科–《卡特兰数》找到了答案:
刚刚那个解释是我们比较常规的想法,但这道题也可以不管出栈后的数序是什么,而是把问题转移到有多少种情况满足n个数正常的入栈出栈,我们设入栈为1,出栈为0,用10表示我们出栈入栈的操作方案,也就要保证在每一次操作时1的个数都要大于等于0,这就和之前学卡特兰数解决的01问题一模一样了,所以f(n)=C(2n,n)-C(2n,n-1)
,也可以变成f(n)= C(2n,n) / (n+1)
如果应用到实际问题:
比如售票问题:2n个人排成一行进入剧场,入场费5元。有n个人有一张十元,n个人有一张五元,剧院无其它钞票,问有多少种方法使得只要有10元的人买票,售票处就有5元的钞票找零(将持5元者到达视作将5元入栈,持10元者到达视作使栈中某5元出栈,保证已进入剧场的五元持有者每时每刻大于等于已进入剧场的十元持有者)
#include<iostream>
using namespace std;
const int N = 20;
int h[N];
int main()
{
int n;
cin >> n;
h[0] = 1;
//Init,当没有序列,无法入栈无法出栈算一种
//真要说为什么初始化我也不懂,就是通过f[0]加上下面的程序我可以得到f[1]和f[2],就这么写了
for (int i = 1; i <= n; i++)
for (int k = 1; k <= i; k++)
h[i] += h[i - k] * h[k - 1];
//这样写肯定不如直接用卡特兰数的公式(含组合数)+高精度,以防n
cout << h[n];
return 0;
}
方法二:搜索
dalao们实在太强了,蒟蒻瑟瑟发抖,看来只能多接触题才能吸收dalao们的🐂🍺思想。
设一个二维数组f[rev][hav],rev表示待参与入栈出栈的数的个数,hav代表已经在栈内的元素个数。
然后在进行操作的时候有三种情况需要考虑:
1.如果当前f[rev][hav]已经存在就没有必要继续搜索,直接返回就行。
边界情况,如果hav为0,也就是说没有数待排了,如果栈空那么结果就是已经出栈的序列,如果非空就是已有序列加上栈里面的元素一个个出栈(顺序只有这一种),这种情况只要一种,所以返回return1
。
2.除了边界以外,考虑一下当前栈是否为空,如果栈为空且还有数待处理,那么此时栈是不能出栈的,只能入栈,所以return f[rev,hav]+=dfs(rev-1,hav+1)
。
3.如果栈非空,那么有两种情况,一种是直接出栈(栈订元素补充到出栈的序列后面)dfs(hav-1,rev),第二种先入栈,那么之前的栈顶元素就不可能接在原始的序列后面,只要新入栈的元素是直接出栈还是继续下一个元素入栈由dfs(hav+1,rev-1)
考虑。(关键就在于栈顶元素是否会接在原始序列后面)
f[rev][hav] += dfs(rev,hav - 1) + dfs(rev - 1,hav + 1);
主要是思路难想,知道了思路代码就很好写了。
上代码:
#include<iostream>
using namespace std;
const int N = 20;
int f[N][N];
int dfs(int rev, int hav)//rev个元素待排,栈中已有hav个元素
{
if (f[rev][hav]) return f[rev][hav];
if (!rev) return 1;
//有0个待排元素,如果栈为空已经出栈的序列就是结果,如果非空也只能一个个出栈
if (!hav) return f[rev][hav] += dfs(rev - 1, hav + 1);
//栈空的,没法出栈,只能入栈
else return f[rev][hav] += dfs(rev,hav - 1) + dfs(rev - 1,hav + 1);
//栈非空,可以直接出栈dfs(rev,hav - 1),也可以入栈dfs(rev - 1,hav + 1)
}
int main()
{
int n;
cin >> n;
cout << dfs(n, 0);
return 0;
}
感觉dp这边最难的就是状态转移方程和边界值…好难啊…