DP题单解题报告合集
DP大复习计划
题单链接:\(link\)
背包部分
虽然有些题和背包没什么关系。
P1284 三角形牧场(12.27)
由于本题要把木板用完,所以只要确定两边就可以确定一个三角形
本题数据范围小,我们可以预处理出两边长是哪些的三角形是可构成的。
状态定义:f[k][i][j]为由前 \(k\) 块木板构成且两边长为 \(i,j\) 的 三角形的所有方案。属性是bool(只有存在/不存在两种取值)
状态计算:\(f[k][i][j]=f[k-1][i-l[k]][j]\,or\,f[k-1][j-l[k]] \,or\,f[k-1][i][j]\)
翻译一下就是枚举到 \(k\) 块木板,两边为 \(i,j\) 的三角形能被构成当且仅当前 \(k-1\) 块木板下两边为 \(i,j\) 或 \(i-l[k],j\) 或 \(i,j-l[k]\) 能构成。
注意到转移方程 \([k]\) 维只与 \([k-1]\) 维有关,所以可以滚掉。然后枚举边长就好了。
在枚举边长时,任意一边边长一定不会超过总长的一半。
做完DP再枚举一下边长暴力打擂台求面积最大值就行了。
#include <bits/stdc++.h>
using namespace std;
typedef long double LD;
typedef long long ll;
const int N=1010;
int n;
ll l[N],C;
LD S(int i,int j)
{
LD k=C-i-j;
LD p=(LD)C/2;
return sqrt(p*(p-i)*(p-k)*(p-j));
}
bool f[N][N];
int main()
{
scanf("%d",&n);
for(int i=1; i<=n; i++)
scanf("%lld",&l[i]),C+=l[i];
f[0][0]=1;
LD ans=-1.0;
for(int k=1; k<=n; k++)
{
for(int i=C/2+1; i>=0; i--)
{
for(int j=i; j>=0; j--)
if((j>=l[k]&&f[i][j-l[k]])||(i>=l[k]&&f[i-l[k]][j])) f[i][j]=1;
}
}
for(int i=1; i*2<=C; i++)
{
for(int j=1; j*2<=C; j++)
{
if(f[i][j]) ans=max(ans,max(ans,S(i,j)));
}
}
if(ans!=-1.0) ans*=100,printf("%lld",(ll)ans);
else printf("-1");
return 0;
}
P1156 垃圾陷阱(12.27)
稍微分析一下题目,垃圾下落的时间点,奶牛无论是吃掉或者填上,一定要立刻处理掉,这样一定是最优的。
所以时间没有太大影响。
观察题面,试设 \(dp[i][j]\) 为前 \(i\) 个垃圾达到高度 \(j\) 所能有的最大生命值。
若是要翻出去,就必须把高度填满(或填爆),如果所有的状态都无法活着翻出去,也就是说,对于任意 \(j+a[i].h\ge D\) 没有 \(i\) 使得\(dp[i][j] \ge 0\),就翻不出去,那就把所有垃圾全部吃掉以换取最大存活时间。
状态就应该如下转移
\(dp[i][j]=max(dp[i][j],dp[i-1][j]+a[i].v-(a[i].t-a[i-1].t))(吃,不填)\)
\(dp[i][j+a[i].h]=max(dp[i][j],dp[i-1][j]-(a[i].t-a[i-1].t))(不吃,填)\)
如果有一个 \(dp[i][j]\ge 0且j+a[i].h\ge D\) ,那么就走出去了,
不然就在 \(dp[i][0]+a[i].t\) 中取最大值就好了。
\(dp[0][0]\) 初始化成 \(10\)。
滚动数组爱加不加。
但是为什么下面那个一定要用刷表法呢QAQ
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
struct node
{
int t,v,h;//时间生命与高度
} a[110];
bool cmp(node x,node y)
{
return x.t<y.t;
}
int n,m;
int dp[1010][1010];
int main()
{
scanf("%d%d",&m,&n);
for(int i=1; i<=n; i++)
scanf("%d%d%d",&a[i].t,&a[i].v,&a[i].h);
sort(a+1,a+1+n,cmp);
memset(dp,~0x3f,sizeof dp);
dp[0][0]=10;a[0].t=0;
int ans=-1;
for(int i=1;i<=n;i++)
{
for(int j=m;j>=0;j--)
{
if(dp[i-1][j]<a[i].t-a[i-1].t) continue ;
if(j+a[i].h>=m)
{
printf("%d",a[i].t);
return 0;
}
dp[i][j+a[i].h]=max(dp[i][j+a[i].h],dp[i-1][j]-(a[i].t-a[i-1].t));//刷表法
dp[i][j]=max(dp[i][j],dp[i-1][j]+a[i].v-(a[i].t-a[i-1].t));
}
ans=max(ans,dp[i][0]+a[i].t);
}
printf("%d",ans);
return 0;
}
P4158 [SCOI2009]粉刷匠(12.28)
先试图独立每一行:设 \(g[i][j][k]\) 为第 \(i\) 行前 \(j\) 个格子涂了 \(k\) 次的正确格数最大值。
对于一个状态 \(g[i][j][k]\) 它可能由 \(g[i][1 \sim j-1][k-1]\) 中的任意一个状态转移而来。假设这个状态是由 \(q\) 转移过来的,那么转移代价是 \(+max(sumRed[j]-sumRed[q-1],sumBlue[j]-sumBlue[q-1])\) ,即这一次涂红色/蓝色 (不会考虑不涂,无论涂什么颜色都优于或至少等于不涂) 所能获得的正确格数(\(sumRed\ ,\ sumBlue\) 是当前行红色/蓝色格子个数的前缀和)。
整个状态转移方程:
暴力处理的话应该是 \(O(nm^2T)\) 。(枚举 \(i(i \sim n),j(1 \sim m),q(1 \sim j-1),k(1 \sim T)\) )
现在每一行的最大值都被求出来了。
我们可以把每一行看做一个泛化物品,涂色次数就是费用,得到的最多格数就是价值。标准的泛化物品背包。
继续暴力处理,时间 \(O(nmT)\)
然后愉快T掉一点...
这里其实有一个小优化,\(q\) 是可以从 \(k-1\) 开始枚举的,因为涂了 \(k-1\) 次,必定至少有 \(k-1\) 个格子被涂掉,所以 \(k-1\) 之前的状态没用,可以减掉冗余。
#include<bits/stdc++.h>
using namespace std;
int g[55][55][2510];
int f[2510];
int sumred[55][2510];
int sumblue[55][2510];
int n,m,t;
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>t;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
char q;
cin>>q;
sumred[i][j]=sumred[i][j-1];
sumblue[i][j]=sumblue[i][j-1];
if(q=='0') sumred[i][j]++;
else if(q=='1') sumblue[i][j]++;//统计前缀和
}
}
for(int i=1;i<=n;i++)
{
for(int k=1;k<=m;k++)//每行的费用最多为 m
{
for(int j=1;j<=m;j++)
{
for(int q=k-1;q<j;q++)
{
g[i][j][k]=max(g[i][j][k],g[i][q][k-1]+max(sumred[i][j]-sumred[i][q],sumblue[i][j]-sumblue[i][q]));//处理泛化物品
}
}
}
}
for(int i=1;i<=n;i++)
{
for(int j=t;j>=0;j--)
{
for(int k=0;k<=min(j,m);k++)
{
f[j]=max(f[j],f[j-k]+g[i][m][k]);
}
}
}
int ans=0;
for(int i=0;i<=t;i++)
ans=max(ans,f[i]);
cout<<ans;
return 0;
}
P4095 [HEOI2013]Eden 的新背包问题(12.28)
缺物+多重背包。
我们知道如果不管删除的话它就是一个物品使用次数较多的多重背包,套个二进制优化就可做。
但是现在多了个删除该怎么办?
多了个删除,其实就是在问 \([1,i)\) 与 \((i,n]\) 的物品
我们可以如法炮制,再用一个 \(f_2\) 从后往前推。
答案就是 \(max\{\ f1[i-1][a]+f2[i+1][b]\ \}\quad (a+b=m)\)
注意玩偶从 \(0\) 开始编号!!
先贴个二进制拆分的板子
for(int i=1;i<=n;i++)
{
for(int j=1;j<=p[i];j<<=1)//二进制分堆,此时j相当于权重,即这个二进制拆分下来物品价值的系数
{
/*do something*/
p[i]-=j;
}
if(p[i]>0)//若c还有剩余,再来一次01pack
{
/*do something*/
}
}
二进制拆分是需要额外的空间的,建议拆分后的新数组开大一点,本题20倍绝对够了。
code:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1010;
int c[N],v[N],p[N];
ll nc[N*20],nv[N*20],no[N*20],tot=0;
ll f[N*20][N],g[N*20][N];
int n,m=1000,t;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d%d%d",&c[i],&v[i],&p[i]);
for(int i=1;i<=n;i++) //二进制拆分
{
for(int j=1;j<=p[i];j<<=1)
{
p[i]-=j;
nc[++tot]=(ll)c[i]*j;
nv[tot]=(ll)v[i]*j;
no[tot]=(ll)i;
}
if(p[i]>0)
{
nc[++tot]=(ll)c[i]*p[i];
nv[tot]=(ll)v[i]*p[i];
no[tot]=(ll)i;
}
}
/*这里的拆分方式,决定了由同一个物品拆出来的新物品编号一定连续*/
for(int i=1;i<=tot;i++)//在新背包中直接DP
{
for(int j=m;j>=0;j--)
{
if(j-nc[i]>=0)
f[i][j]=max(f[i-1][j],f[i-1][j-nc[i]]+nv[i]);
else f[i][j]=f[i-1][j];
}
}
for(int i=tot;i>=1;i--)
{
for(int j=m;j>=0;j--)
{
if(j-nc[i]>=0)
g[i][j]=max(g[i+1][j],g[i+1][j-nc[i]]+nv[i]);
else g[i][j]=g[i+1][j];
}
}
scanf("%d",&t);
for(int i=1;i<=t;i++)
{
int d,x;
scanf("%d%d",&d,&x); d++;
int l=0,r=0;
while(no[l+1]<d&&l<=tot) ++l,++r;
while(no[r+1]<=d&&r<=tot) ++r;//寻找区间边界
ll ans=0;
for(int i=0;i<=x;i++)
{
ans=max(ans,f[l][i]+g[r+1][x-i]);
}
printf("%lld\n",ans);
}
return 0;
}
P4141 消失之物(12.28)
缺物+求方案数。
理论上应该也可以像上一道题那样做,但是没有太大必要。
应该已经知道,对于求方案数的问题的状态转移方程是:
\(f[\ i\ ][\ j\ ]=sum\{\ f[\ i-1\ ][\ j\ ]+f[\ i\ ][\ j-v[\ i\ ]\ ]\ \}\)。
那么如果放弃 \(i\) 物品,则相当于直接递推的最终答案多了所有的 \(f[\ i\ ][\ j-v[\ i\ ]\ ]\)
于是在递推到这一层时减掉就行了。
核心操作单独拿出来
for(int i=1;i<=n;i++)
{
for(int j=m;j>=0;j--)
{
f1[j]+=f1[j-v[i]];//第一遍dp
}
memcpy(f2,f1,sizeof f1);
for(int j=m;j>=0;j--)
{
f2[j]-=f2[j-v[i]];//顺推回去
}
}
code:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=20200;
int n,m;
ll v[N];
ll f[N],g[N];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%lld",&v[i]);
f[0]=1;
for(int i=1;i<=n;i++)
{
for(int j=m;j>=v[i];j--)
f[j]=(f[j]+f[j-v[i]])%10;//模10 == 80->100
}
for(int i=1;i<=n;i++)
{
memcpy(g,f,sizeof f);
for(int j=v[i];j<=m;j++)
g[j]=g[j]-g[j-v[i]];
for(int j=1;j<=m;j++)
printf("%lld",(g[j]%10+10)%10);
printf("\n");
}
return 0;
}
P2340 [USACO03FALL]Cow Exhibition G(12.28)
乍一看像是一个二维价值的背包??
情商和智商和的最大值,但是要求情商和智商和都为正数。
我们可以试着让其中一维变成代价,这样可能好处理得多。
令 \(c_i\) 是第 \(i\) 只牛的智商, \(v_i\) 是情商。
假设智商作为代价, \(f(i,j)\) 代表在前 \(i\) 头奶牛中智商和为 \(j\) 的时候的最大情商和。
很快得到状态转移方程:\(f(i,j)=max\{\ f(i-1,j)\ ,\ f(i-1,j-c_i)+v_i\ \}\)
\(dp[0][0]=0\) ,其他全是 \(-\infty\)。
标准的01pack写法。
转头一看范围 \(400\times 40w\) MLE直接芜湖
现在看看有什么问题
-
1.这个很显然是可以压成一维的。
-
2.\(c_i\) 可能为负,那枚举顺序怎么办呢?
解决方法:判断 \(c_i\) 正负,正的就倒着枚举,负的就正着枚举。
-
3.对于最优解,其智商和中途可能小于 \(0\) ,数组下标会越界啊
解决方法:数组下标右移 \(40w\) 。空间方面,前面你是压了维的,肯定够。
但是状态的定义有所变化:\(f(i,j)\) 代表在前 \(i\) 头奶牛中智商和为 \(j-4\times 10^5\) 的时候的最大情商和。
现在来看一下最终答案怎么求。
我们已经求出来了每个智商和的情商和最大值了,枚举所有出现的正智商和值,加上它们对应的最大情商和值,取 \(max\) 。
总时间复杂度: \(O(nm)\) ,\(m\) 取决于你估计的最大值。
分析过程草稿截图 (鼠标写字日渐熟练)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=410;
const int M=400000;
int n;
ll c[N],v[N];
ll f[M*3+200];//反正不卡空间,大一点保险
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%lld%lld",&v[i],&c[i]);
memset(f,-0x3f,sizeof f);
f[M]=0;
for(int i=1;i<=n;i++)
{
// if(c[i]<0&&v[i]<0) continue;//双低直接跳过,加它绝对不最优
if(c[i]>0)
{
for(int j=M;j>=-M;j--)
{
if(j-c[i]>=-M&&j-c[i]<=M)
f[j+M]=max(f[j+M],f[j-c[i]+M]+v[i]);
}
}
else
{
for(int j=-M;j<=M;j++)
{
if(j-c[i]>=-M&&j-c[i]<=M)
f[j+M]=max(f[j+M],f[j-c[i]+M]+v[i]);
}
}
}
ll ans=0;
for(int i=0;i<=M;i++)
{
if(f[i+M]<0) continue;
ans=max(ans,i+f[i+M]);
}
printf("%lld",ans);
return 0;
}
P3188 [HNOI2007]梦幻岛宝珠(1.3)
我不要宝珠了行不行啊QAQ
超大背包+物品问题。
物品数量很少,并且 \(w_i,v_i\) 可以拆分成 \(a\times 2^b\) 的形式。
考虑二进制分组
我们可以按照 \(b\) 的大小对物体进行分组,然后在每个组内01背包,就能得到一些泛化物品 (\(link:泛化物品的定义\))
-
泛化物品定义: 考虑一种物品,它没有固定的费用和价值,而是其价值随着分配给它的费用变化而变化。
对于一个泛化物品,可以用一个一维数组\(G_i\)表示其费用与价值的关系:当费用为\(i\)时,相对应的价值为\(G_i\)。
-
泛化物品的和:把两个物品合在一起的运算,就是枚举费用分配给两个物品,
\(G_j=max(G1_{j-k},G2_{k})(0 \leq k \leq j \leq C)\)
时间复杂度为\(O(C^2)\)。
\(g(i\,,\,j)\) 表示 \(2^i\) 组分配给 \(j\) 的费用时能获得的最大价值。
泛化物品的个数最多只有 \(31\) 个,因为 \(0\le b \le 30\)
然后我们考虑将泛化物品合并得到答案。
根据题目,我们可以定义状态值 \(f(i\,,\,j)\) 为 用了 \(j\times 2^i + W \& (2^i-1)\) 的容积
也就是说,我们枚举费用二进制下的最高位,剩下的位与题中的 \(W\) 相同。
设 \(W\) 的最高位代表 \(2^I\) ,答案就是 \(f(I\,,\,1)\)。
枚举 \(i,j\) 为 分组到达的位置 和 使用的总费用,枚举 \(k\) 为 当前组所使用的费用。
则状态转移方程:
\(f(i\,,\,j)=max\{g(i\,,\,k)+f(i-1\,,\,(j-k)\times 2+(W>>(i-1)\&1)) \}\)
注意 \(i\) 与 \(i-1\) 联系的时候要将当前 \(j\) 中分配给上一组的费用 \(\times 2+\,\) \(W\)上一二进制位上的数。
\(f,g\)记得开long long
于是打出如下代码(C++11 O2才过掉的暴力代码)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=5020;
const int L=210;
int n,W;
int maxn=0;
vector<ll> w[N],v[N];
ll f[L][N],g[L][N];
inline ll max_(ll a,ll b)
{
return (a>b?a:b);
}
int main()
{
while(scanf("%d%d",&n,&W)!=EOF && ~n && ~W)
{
for(int i=0;i<=100;i++)
w[i].clear(),v[i].clear();
memset(f,0,sizeof f);
memset(g,0,sizeof g);
for(int i=1;i<=n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
int j=0;
while(!(x&1)) x>>=1,++j;
maxn=max_(x,j);
w[j].push_back(x),v[j].push_back(y);//分组
}
int I=0;
while((1<<I)<W) I++;//求 W 最高位数
I--;
for(int i=0;i<=I;i++)//遍历每个组
{
if(!w[i].size()) continue;
for(unsigned long long j=0;j<w[i].size();j++)
{
for(int k=1000;k>=w[i][j];k--)
{
g[i][k]=max_(g[i][k],g[i][k-w[i][j]]+v[i][j]);
}
}
}
for(int i=0;i<=I;i++)
{
for(int j=1000;j>=0;j--)
{
for(int k=0;k<=j;k++)//枚举当前组使用的费用
f[i][j]=max_(f[i][j],(ll)g[i][k]+f[i-1][(j-k)*2+(W>>(i-1)&1)]);
}
}
printf("%lld\n",f[I][1]);
}
return 0;
}
P1941 [NOIP2014 提高组] 飞扬的小鸟
不愧是NOIp tg。
uysy我第一眼看到这个题感觉像是模拟
第二眼想到了每两根管道之间做一次背包,然后假了((((((
设状态 \(f(i,j)\) 表示 到达横坐标为 \(i\) 纵坐标为 \(j\) 时的位置。
每向左移动一个单位长度,可以向上 \(q\times x\) ( \(q\) 为点击次数) 个单位长度,或向下移动 \(y\) 个单位长度。可以得到向上是完全背包,向下是 \(01\) 背包。
具有向上和向下两种情况,理应使用预设型DP,也就是应该再开一维 \([0/1]\) 来辨别决策。
但由于每次决策之间互不干扰(即无后效性)且我们总是在 \(0/1\) 之间取最小值所以没什么必要。
思考状态转移方法。
在每次转移时若遇到管道则将管道占据的空间全部设为非法 (inf),保证状态不会由此转移。
对于触顶,要注意特判
区间DP
完了什么都不会了
P4170 [CQOI2007]涂色(1.3)
算是比较模板的题。
设状态: \(f(i\,,\,j)\) 表示区间 \([\ i\ ,\ j\ ]\) 染色完成所需要的最小步数。
这里要分类讨论:
-
当 \(i=j\) 时:
显然 \(f(i\,,\,j)=1\)。
-
当 \(i\ne j\ and\ s[i]=s[j]\) 时
我们只需要在首次涂色的基础上多涂一格就是
\(f(i\,,\,j)= min(\ f(i+1\,,\,j)\ ,\ f(i\,,\,j-1)\ )\)
-
当 \(i\ne j\ and\ s[i]\ne s[j]\) 时
考虑枚举分割点 \(k\),将两边分别正确涂色所需要的的步数和的最小值就是答案。
\(f(i\,,\,j)=min\{\ f(i\,,\,k)+f(k+1\,,\,j)\ \}\)
#include <bits/stdc++.h>
using namespace std;
const int N=110;
int f[N][N];
char s[N];
int main()
{
memset(f,0x3f,sizeof f);
cin>>s;
int n=strlen(s);
for(int len=1;len<=n;len++)
{
for(int i=0;i+len-1<n;i++)
{
int j=i+len-1;
if(i==j) f[i][j]=1;
else if(i!=j&&s[i]==s[j]) f[i][j]=min(f[i+1][j],f[i][j-1]);
else if(i!=j&&s[i]!=s[j])
{
for(int k=i;k<j;k++)
{
f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]);
}
}
}
}
printf("%d",f[0][n-1]);
return 0;
}