最近准备把dp完完整整的复习一遍,开博记录
前言
记忆化搜索
- 记忆化搜索的定义
-
· 不依赖任何形式的外部变量
-
· 答案以返回值而非参数形式存在
-
· 对于相同参数返回值相同
- 与动态规划的关系
-
递归实现转移,因此是反向的
- 如何写记忆化搜索
-
- 方法一
-
把这道题的dp状态和方程写出来
-
根据他们写出dfs函数
-
添加记忆化数组
-
- 方法二
-
写出这道题的暴搜程序(最好是dfs)
-
将这个dfs改成"无需外部变量"的dfs
-
添加记忆化数组
动态规划的基本解题思路
- 四个步骤
- 确定子问题
- 定义状态
- 转移方程
- 统计答案/避免重复求解
具体过程详见下文 P2758 编辑距离
背包问题
01背包
01背包的空间优化问题
可以空间优化的根本原因:
第i个状态仅能转移到i-1个
即当一层状态更新完毕,就不会影响其余的状态
如果正向枚举
不满足此性质
P1048 采药
记忆化搜索
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
typedef unsigned long long ull;
int const maxn=5010,maxm=5010,inf=0x1f1f1f1f;
int n,t,val[maxn],cost[maxn],ans[maxn][maxn];
int dfs(int vleft,int step)
{
if(ans[step][vleft]!=-1)
return ans[step][vleft];
if(step>n)
return ans[step][vleft]=0;
int nput=-inf,put=-inf;
nput=dfs(vleft,step+1);
if(cost[step]<=vleft)
put=dfs(vleft-cost[step],step+1)+val[step];
return ans[step][vleft]=std::max(nput,put);
}
int main()
{
memset(ans,-1,sizeof(ans));
scanf("%d%d",&t,&n);
for(int i=1;i<=n;i++)
scanf("%d%d",&cost[i],&val[i]);
printf("%d",dfs(t,1));
return 0;
}
递推
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
typedef unsigned long long ull;
int const maxn=50100,maxm=50100,inf=0x1f1f1f1f;
int n,t,val[maxn],cost[maxn],ans[maxn];
int main()
{
scanf("%d%d",&t,&n);
for(register int i=1;i<=n;i++)
scanf("%d%d",&cost[i],&val[i]);
for(register int i=1;i<=n;i++)
for(register int j=t;j>=cost[i];j--)
ans[j]=std::max(ans[j],ans[j-cost[i]]+val[i]);
printf("%d",ans[t]);
return 0;
}
P1510 精卫填海
一开始读错了题,以为要刚好填满,就想把体积作为v,把问题转化成可行性背包
事实上该题可以把体积作为w,每当f[j]>W时,统计一下答案
#include<iostream>
#include<cstdio>
int const maxn=10101,maxm=10101,inf=0x1f1f1f1f;
int W,n,V,cv[maxn],w[maxn],f[maxn],ans;
int main()
{
ans=-inf;
scanf("%d%d%d",&W,&n,&V);
for(int i=1;i<=n;i++)
scanf("%d%d",&w[i],&cv[i]);
for(int i=1;i<=n;i++)
for(int j=V;j>=cv[i];j--)
{
f[j]=std::max(f[j],f[j-cv[i]]+w[i]);
if(f[j]>=W)
ans=std::max(ans,V-j);
}
if(ans<0)
{
puts("Impossible");
return 0;
}
printf("%d",ans);
return 0;
}
P1566 加等式
模型竟然是可行性01背包求解方案数问题,完全没看出来…
题意其实是取某些数使他们的和等于集合内的某个数,每个数只能取一次,问有多少种方案,这样就很明显了
把集合内最大的数作为上限,背包必须填满
跑完背包只要把每个数对应的背包加起来即可
注意!要把自己相等的方案减去
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
int const maxn=501,maxm=501,inf=0x1f1f1f1f;
int t,n,f[1110],a[maxn],ans,mx;
int main()
{
scanf("%d",&t);
while(t--)
{
memset(f,0,sizeof(f));
ans=0;
mx=-1;
scanf("%d",&n);
f[0]=1;
for(int i=1;i<=n;i++)
scanf("%d",&a[i]),mx=std::max(mx,a[i]);
for(int i=1;i<=n;i++)
for(int j=mx;j>=a[i];j--)
f[j]+=f[j-a[i]];
for(int i=1;i<=n;i++)
ans+=f[a[i]];
printf("%d\n",ans-n);
}
return 0;
}
P1504 积木城堡
还算有意思的可行性01背包
然而这种水题我竟然没看出来
对于每一堆积木,都求一下可行性,用桶统计一下每个高度的可行性
当某个高度的的可行性达到n种,说明n堆积木都能凑出这个高度,作为一个可行解
从高到低枚举保证答案最优
没有可行解要特判
#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=1110,maxm=100110,inf=0x1f1f1f1f;
int f[maxm],cv[maxn];
int n,sum,mn=inf,cnt,ton[maxm];
int min(int x,int y,int z)
{
return std::min(std::min(x,y),z);
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
memset(f,0,sizeof(f));
f[0]=1;
cnt=0,sum=0;
for(int x;;)
{
scanf("%d",&x);
if(x==-1)
break;
cv[++cnt]=x;
sum+=x;
}
mn=std::min(mn,sum);
for(int j=1;j<=cnt;j++)
for(int k=sum;k>=cv[j];k--)
f[k]=std::max(f[k],f[k-cv[j]]);
for(int k=1;k<=sum;k++)
ton[k]+=f[k];
}
for(int k=mn;k>=0;k--)
if(ton[k]==n)
{
printf("%d",k);
return 0;
}
printf("0");
return 0;
}
完全背包
P1474 货币系统
完全背包统计可行性方案数问题
其实不是特别明白…
感性理解一下就是,选择某个面值,就能获得组成当前面值的方案数
因为选择某个数,方案数是不会改变的
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
typedef long long ll;
int const maxn=10110,maxm=10110,inf=0x1f1f1f1f;
int n,T;
ll f[maxn],cv[maxn];
int main()
{
scanf("%d%d",&n,&T);
for(int i=1;i<=n;i++)
scanf("%lld",cv+i);
f[0]=1;
for(int i=1;i<=n;i++)
for(int j=cv[i];j<=T;j++)
f[j]+=f[j-cv[i]];
printf("%lld",f[T]);
return 0;
}
P2904 River Crossing
比较显然的背包问题
把奶牛个数看成容积,耗时看做价值
问题转化为了可行性最小背包问题
由于同一奶牛个数可以重复选,比如可以每次只带一只奶牛所以是完全背包
#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=2511,maxm=2511;
int f[maxn],pre,n,m;
int main()
{
memset(f,0x1f,sizeof(f));
scanf("%d%d",&n,&m);
f[0]=-m,pre=m;
for(int w,i=1;i<=n;i++)
{
scanf("%d",&w),pre+=w;
for(int j=i;j<=n;j++)
f[j]=std::min(f[j],f[j-i]+pre+m);
}
printf("%d",f[n]);
return 0;
}
P2725 Stamps
一开始没看到从1开始…
而且价值跟容积完全搞反了…
可行性完全背包
f[i]表示装到价值为i最少需要多少邮票
当f[j-w]>=sum时不能转移
#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=54,maxm=2001000,inf=0x1f1f1f1f;
int f[maxm];
int n,sum;
int main()
{
memset(f,0x1f,sizeof(f));
f[0]=0;
scanf("%d%d",&sum,&n);
for(int w,i=1;i<=n;i++)
{
scanf("%d",&w);
for(int j=w;j<=maxm;j++)
{
if(f[j-w]>=sum)
continue;
f[j]=std::min(f[j],f[j-w]+1);
}
}
for(int i=1;i<=maxm;i++)
if(f[i]==inf)
{
printf("%d",i-1);
return 0;
}
}
分组背包
P2409 Y的积木
分组背包的可行性方案数问题
f[i][j]表示前i种的和为j的方案数
对于每一种,都跑一遍01背包
因为每个种都从上一种转移,所以每一种一定只能选一个
#include<iostream>
#include<cstdio>
#include<cmath>
int const maxn=10011,maxm=111,inf=0x1f1f1f1f;
int f[maxm][maxn],n,K,cv[maxm][maxm],maxx[maxm],minn[maxm],mx,mn;
int main()
{
scanf("%d%d",&n,&K);
for(int i=1;i<=n;i++)
{
maxx[i]=-1;
scanf("%d",&cv[i][0]);
for(int k=1;k<=cv[i][0];k++)
{
scanf("%d",&cv[i][k]);
maxx[i]=std::max(maxx[i],cv[i][k]);
}
mx+=maxx[i];
}
f[0][0]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=cv[i][0];j++)
for(int k=mx;k>=cv[i][j];k--)
f[i][k]+=f[i-1][k-cv[i][j]];
for(int k=1;k<=mx&&K;k++)
{
while(f[n][k]&&K)
{
f[n][k]--,K--;
printf("%d ",k);
}
}
return 0;
}
二维费用背包
相当于有两个限制条件的背包
基本转移如下
for(int i=1;i<=n;i++)
for(int j=下限1;j<=限制1;j++)
for(int k=下限2;k<=限制2;k++)
f[j][k]=std::max(f[j][k],f[j-cv1[i]][k-cv2[i]);
P1759 通天之潜水
裸的二维费用背包+输出路径
#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=111,maxm=210,inf=0x1f1f1f1f;
int f[maxm][maxm],cv1[maxm],cv2[maxn],w[maxn],path[maxm][maxm][maxn];
int n,V1,V2;
int main()
{
scanf("%d%d%d",&V1,&V2,&n);
for(int i=1;i<=n;i++)
scanf("%d%d%d",&cv1[i],&cv2[i],&w[i]);
for(int i=1;i<=n;i++)
for(int j=V1;j>=cv1[i];j--)
for(int k=V2;k>=cv2[i];k--)
{
int lst1=j-cv1[i],lst2=k-cv2[i];
if(f[j][k]<f[lst1][lst2]+w[i])
{
f[j][k]=f[lst1][lst2]+w[i];
path[j][k][0]=path[lst1][lst2][0]+1;
path[j][k][path[j][k][0]]=i;
for(int l=1;l<=path[lst1][lst2][0];l++)
path[j][k][l]=path[lst1][lst2][l];
}
}
printf("%d\n",f[V1][V2]);
for(int i=1;i<=path[V1][V2][0];i++)
printf("%d ",path[V1][V2][i]);
return 0;
}
P1586 四方定理
二维费用方案数背包
一开始试图用Y的积木的思路写,给他分成四组
结果这道题要求不超过四个,所以不能这么做
虽然这两者很像,但还是略有不同
分组背包的状态表示的是考虑完i组限制j能得到的答案
二维费用背包表示考虑完限制i后限制j能得得到的答案
(根据这道题的特殊性,我们需要把每个限制i得到的答案加起来)
#include<cstdio>
#include<iostream>
using namespace std;
int f[5][32770],n,t;
int main()
{
f[0][0]=1,n=32768;
for(int i=1;i*i<=n;i++)
for(int j=i*i;j<=n;j++)
for(int k=1;k<=4;k++)
f[k][j]+=f[k-1][j-i*i];
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
int ans=0;
for(int k=1;k<=4;k++)
ans+=f[k][n];
printf("%d\n",ans);
}
return 0;
}
混合背包
把上述所有背包给合起来即可
P2623 物品选取
分组背包+多重背包+完全背包
#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=2112,inf=0x1f1f1f1f;
int n,V,f[maxn];
int min(int x,int y,int z)
{
return std::min(std::min(x,y),z);
}
void Pro(int a,int b)
{
for(int j=V;j>=0;j--)
for(int v=0;v<=j;v++)
{
int w=a*v*v-b*v;
f[j]=std::max(f[j],f[j-v]+w);
}
}
void Pzo(int v,int w)
{
for(int j=V;j>=v;j--)
f[j]=std::max(f[j],f[j-v]+w);
}
void Pcp(int v,int w)
{
for(int j=v;j<=V;j++)
f[j]=std::max(f[j],f[j-v]+w);
}
void Pmu(int v,int w,int nm)
{
if(v*nm>=V)
{
Pcp(v,w);
return;
}
int k=1;
while(k<=nm)
{
Pzo(v*k,w*k);
nm-=k;
k*=2;
}
Pzo(v*nm,w*nm);
}
int main()
{
scanf("%d%d",&n,&V);
for(int op,w,cv,num,i=1;i<=n;i++)
{
scanf("%d%d%d",&op,&w,&cv);
if(op==1)
Pro(w,cv);
else if(op==2)
{
scanf("%d",&num);
Pmu(cv,w,num);
}
else
Pcp(cv,w);
}
printf("%d",f[V]);
return 0;
}
坐标DP
P1434 滑雪
- 记搜写法
简单好用
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
typedef unsigned long long ull;
int const maxn=111,maxm=111,inf=0x1f1f1f1f;
int const dx[4]={0,0,1,-1};
int const dy[4]={1,-1,0,0};
int map[maxn][maxn],f[maxn][maxn],ans,n,m;
int cmp(int x,int y)
{
return map[x]<map[y];
}
int dfs(int x,int y)
{
if(x>n||x<1||y>m||y<1)
return 0;
if(f[x][y])
return f[x][y];
int nans=1;
for(int p=0;p<4;p++)
{
int nx=x+dx[p],ny=y+dy[p];
if(map[x][y]>map[nx][ny])
nans=std::max(nans,dfs(nx,ny)+1);
}
return f[x][y]=nans;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&map[i][j]);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
ans=std::max(ans,dfs(i,j));
printf("%d",ans);
return 0;
}
- 递推写法
转移与记搜刚好相反,从低向高转移,只要排一下序就能保证某一状态仅对临近状态有贡献,从而保证无后效性
再考虑状态,最朴素的思路是二维数组,然而不太好排序
我们考虑把二维转成一维
把每一行的开头接在上一行的末尾
这样就会得到一个这样的表格
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
ai | 1 | 2 | 3 | 4 | 5 | 16 | 17 | 18 | 19 | 6 | 15 | 24 |
我们发现一个数x的上下左右有这样的关系,于是就可以转移了
x-m-1 | x-m | x-m+1 |
---|---|---|
x-1 | x | x+1 |
x+m-1 | x+m | x+m+1 |
- 注意边界!
- 当且仅当x-m≤0时,x位于最上一行;
- 当且仅当x+m>n*m时,x位于最下一行;
- 当且仅当x mod m=0时,x位于最右一行;
- 当且仅当(X-1) mod m =0时,x位于最左一行。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
typedef unsigned long long ull;
int const maxn=10110,maxm=10110,inf=0x1f1f1f1f;
int const dx[4]={0,0,1,-1};
int const dy[4]={1,-1,0,0};
int map[maxn],a[maxn],ans,f[maxn];
int cmp(int x,int y)
{
return map[x]<map[y];
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
int len=n*m;
for(int i=1;i<=len;i++)
scanf("%d",&map[i]),a[i]=i;
// for(int i=1;i<=len;i++)
// printf("!!!%d %d\n",i,map[i]);
std::sort(a+1,a+1+len,cmp);
for(int i=1;i<=len;i++)
{
// printf("%d\n",a[i]);
int x=a[i];
f[x]=1;
if(x-m>0 && map[x]>map[x-m])
f[x]=std::max(f[x],f[x-m]+1);
if((x-1)%m && map[x]>map[x-1])
f[x]=std::max(f[x],f[x-1]+1);
if(x+m<=len && map[x]>map[x+m])
f[x]=std::max(f[x],f[x+m]+1);
if(x%m && map[x]>map[x+1])
f[x]=std::max(f[x],f[x+1]+1);
ans=std::max(ans,f[x]);
}
printf("%d",ans);
return 0;
}
P1004 方格取数
- O(n^4)做法
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
typedef unsigned long long ull;
int const maxn=51,maxm=51,inf=0x1f1f1f1f;
int const dx[4]={0,0,-1,-1};
int const dy[4]={-1,-1,0,0};
int const dxf[4]={-1,0,-1,0};
int const dyf[4]={0,-1,0,-1};
int n,m,map[maxn][maxn],f[maxn][maxn][maxn][maxn];
int main()
{
scanf("%d",&n);
for(int x,y,z;;)
{
scanf("%d%d%d",&x,&y,&z);
if(!x&&!y&&!z)
break;
map[x][y]=z;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
for(int k=1;k<=n;k++)
{
int l=i+j-k;
if(l<=0)
continue;
for(int p=0;p<4;p++)
f[i][j][k][l]=std::max(f[i][j][k][l],f[i+dx[p]][j+dy[p]][k+dxf[p]][l+dyf[p]]);
f[i][j][k][l]+=(map[i][j]+map[k][l]);
if(i==k&&j==l)
f[i][j][k][l]-=map[i][j];
}
printf("%d",f[n][n][n][n]);
return 0;
}
Codevs 2853 方格游戏
空间控制在n^3就已经够了
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
typedef unsigned long long ull;
int const maxn=111,maxm=111,inf=0x1f1f1f1f;
int const dx[4]={0,0,-1,-1};
int const dy[4]={-1,-1,0,0};
int const dxf[4]={-1,0,-1,0};
int const dyf[4]={0,-1,0,-1};
int map[maxn][maxn],f[321][maxn][maxn];
int abs(int x)
{
if(x<0)
return -x;
return x;
}
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
scanf("%d",&map[i][j]);
for(int k=1;k<(n<<1);k++)
for(int i=1;i<=std::min(n,k);i++)
for(int j=1;j<=std::min(n,k);j++)
{
for(int p=0;p<4;p++)
{
if(i+dx[p]<1 || !(k-i+dy[p]) || j+dxf[p]<1 || !(k-i+dyf[p]))
continue;
f[k][i][j]=std::max(f[k][i][j],f[k-1][i+dx[p]][j+dxf[p]]);
}
f[k][i][j]+=abs(map[i][k-i+1]-map[j][k-j+1]);
}
printf("%d\n",f[2*n-1][n][n]);
return 0;
}
P1508 likecloud
其实这题根本就是那道时代的眼泪,那道在ioi上横空出世的一道神题
但还是写出锅来了…
边界可以直接memset
必须从第一行开始搜,因为需要给他们赋上点权
从上往下搜比较暴力,从下往上搜写崩了,等等再调吧…
- 从上往下
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
int const maxn=501,maxm=501,inf=0x1f1f1f1f;
int n,m,f[maxn][maxn],ans,w[maxn][maxn];
int max(int x,int y,int z)
{
return std::max(std::max(x,y),z);
}
int main()
{
memset(w,-0x1f,sizeof(w));
scanf("%d%d",&n,&m);
int x=(m/2+1);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&w[i][j]);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
f[i][j]+=max(f[i-1][j-1],f[i-1][j],f[i-1][j+1])+w[i][j];
printf("%d",max(f[n][x],f[n][x+1],f[n][x-1]));
return 0;
}
P2049 魔术棋子
这道题一看似乎是搜索,然而2^n肯定是过不了的
观察数据,k<=100,应该想到是关于剩余容量可能性的dp
f[i][j][k]表示i,j处是否能得到k这个数
最朴素的转移是枚举所有状态,枚举上一次的所有可能性看看能不能得到当前状态,复杂度O(nmk^2)
不过我们发现,枚举当前状态效率很低,因为有很多废状态根本不可能由上一个转移而来吗
所以我们直接枚举上一次的所有状态即可,复杂度O(nmk)
稳如老狗
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
int const maxn=111,maxm=111,inf=0x1f1f1f1f;
int n,m,p,w[maxn][maxn],f[maxn][maxn][maxn],ans,anss[maxn];
int main()
{
scanf("%d%d%d",&n,&m,&p);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&w[i][j]);
f[1][1][w[1][1]%p]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
for(int k=0;k<p;k++)
{
if(f[i][j][k*(w[i][j]%p)%p])
continue;
f[i][j][k*(w[i][j]%p)%p]=f[i-1][j][k]|f[i][j-1][k];
}
for(int k=0;k<p;k++)
{
ans+=f[n][m][k];
if(f[n][m][k])
anss[ans]=k;
}
printf("%d\n",ans);
for(int i=1;i<=ans;i++)
printf("%d ",anss[i]);
return 0;
}
线性DP
P1057 传球游戏
感觉这题递推很神,于是就写了比较友善的记搜
顺便剪了个并无卵用的枝
在条件允许的时候可以两次两次跳,特殊地,有两种可能跳回原地,应注意
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
int const maxn=501,maxm=501,inf=0x1f1f1f1f;
int n,m,f[maxn][maxn];
int dfs(int u,int step)
{
u+=u<=0?n:0;u+=u>n?-n:0;
if(f[u][step])
return f[u][step];
if(step==m)
return f[u][step]=u==1;
return f[u][step]=step==m-1?dfs(u+1,step+1)+dfs(u-1,step+1):dfs(u+2,step+2)+dfs(u-2,step+2)+dfs(u,step+2)*2;
}
int main()
{
scanf("%d%d",&n,&m);
printf("%d",dfs(1,0));
return 0;
}
递推写法
一直以为两层循环1-n在第一层无法转移
结果发现如果1-m在第一层就没问题了
第二次移动一定是由第一次移动更新而来
记搜是枚举点判断次数
递推应该反过来
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
int const maxn=501,maxm=501,inf=0x1f1f1f1f;
int n,m,f[maxn][maxn];
int dfs(int u,int step)
{
u+=u<=0?n:0;u+=u>n?-n:0;
if(f[u][step])
return f[u][step];
if(step==m)
return f[u][step]=u==1;
return f[u][step]=step==m-1?dfs(u+1,step+1)+dfs(u-1,step+1):dfs(u+2,step+2)+dfs(u-2,step+2)+dfs(u,step+2)*2;
}
int main()
{
scanf("%d%d",&n,&m);
f[1][0]=1;
for(int i=1;i<=m;i++)
{
f[1][i]=f[n][i-1]+f[2][i-1];
for(int j=2;j<n;j++)
f[j][i]=f[j-1][i-1]+f[j+1][i-1];
f[n][i]=f[1][i-1]+f[n-1][i-1];
}
printf("%d",f[1][m]);
return 0;
}
P1754 球迷购票问题
很明显,50一定在100前
问题转化为括号匹配,然后直接套卡特兰数
f[i][j]表示已经拿了i张50,j张100
转移考虑对于每一种(i,j)都能从上一张的两种选择更新
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
f[i][j]=f[i-1][j]+f[i][j-1];
只有50的状态为初始状态
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
typedef long long ll;
int const maxn=501,maxm=501,inf=0x1f1f1f1f;
int n;
ll f[maxn][maxn];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
f[i][0]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
f[i][j]=f[i-1][j]+f[i][j-1];
printf("%lld",f[n][n]);
return 0;
}
P1095 守望者的逃离
time是c的关键字!!!
事实证明能用贪心做的dp尽量分着顺序写,每一部分单独考虑
#include<iostream>
#include<cstdio>
int const maxn=300110,maxm=111,inf=0x1f1f1f1f;
int m,s,tim,f[maxn];
int main()
{
scanf("%d%d%d",&m,&s,&tim);
for(int i=1;i<=tim;i++)
{
if(m>=10)
f[i]=f[i-1]+60,m-=10;
else
f[i]=f[i-1],m+=4;
}
//优先处理闪烁
for(int i=1;i<=tim;i++)
{
f[i]=std::max(f[i-1]+17,f[i]);
if(f[i]>=s)
{
puts("Yes");
printf("%d",i);
return 0;
}
}
puts("No");
printf("%d",f[tim]);
return 0;
}
P3399 丝绸之路
事实证明,状态对于动态规划重要性远大于其他!
才不会说我搞了个走到第i个城市休息了j天的状态然后不会初始化就一直锅着
f[i][j]表示第i天在第j个城市处
#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=1011,maxm=111,inf=0x1f1f1f1f;
int n,m,np[maxn],w[maxn],f[maxn][maxn];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&np[i]);
for(int i=1;i<=m;i++)
scanf("%d",&w[i]);
memset(f,0x1f,sizeof(f));
for(int i=0;i<=m;i++)
f[i][0]=0;
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
f[i][j]=std::min(f[i-1][j-1]+w[i]*np[j],f[i-1][j]);
printf("%d",f[m][n]);
return 0;
}
P1387 最大正方形
水题,不过考虑到用二维前缀和直接水岂不是很low做题的初心,还是写了些递推,然而根本不会
因为最大的正方形一定不是以0结尾
所以我们可以用f[i][j]表示右下角为(i,j)的最大正方形
右下角如果为零,显然就不符合状态 if(!a[i][j]) f[i][j]=0;
对于正方形,我们肯定是想考虑他的内部填充,但对于每一次扩大出来的点,还是要处理一下,作为左右边界
因此转移就可以表示为
f[i][j]=min(f[i-1][j-1], 从当前位置向上延伸连续的1的个数, 当前位置左侧延伸连续1的个数)
简化一下会发现后两条就是对正方形边缘的处理
等价于f[i][j]=min(f[i-1][j],f[i][j-1],f[i-1][j-1])+1;
#include<cstdio>
#include<iostream>
int const maxn=111;
int f[maxn][maxn],n,m,ans;
int min(int x,int y,int z)
{
return std::min(std::min(x,y),z);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int a,j=1;j<=m;j++)
scanf("%d",&a),f[i][j]=a?min(f[i-1][j],f[i][j-1],f[i-1][j-1])+a:0,ans=std::max(ans,f[i][j]);
printf("%d",ans);
return 0;
}
P1681 最大正方形2
这题感觉跟最大正方形1一毛一样啊,就是转移条件变了变
f[i][j]表示右下角在(i,j)长度最大正方形
对于每一个f[i][j],我们都希望去考虑他的子结构
包括
- 内部填充
- 向左边延伸
- 向右边延伸
然后对(i,j)这个点特殊处理一下即可(显然只有(a[i][j] a[i-1][j-1]相等 a[i-1][j] a[i][j-1]相等,两对不等才合法)
转移方程
if(!(a[i][j]^a[i-1][j-1]^a[i-1][j]^a[i][j-1]))
f[i][j]=min(f[i-1][j-1],f[i-1][j],f[i][j-1])+1;
最后答案记得+1,原因很显然:没有初始化
#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=1611,inf=0x1f1f1f1f;
int f[maxn][maxn],a[maxn][maxn],n,m,ans;
int min(int x,int y,int z)
{
return std::min(std::min(x,y),z);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&a[i][j]);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
if(!(a[i][j]^a[i-1][j-1]^a[i-1][j]^a[i][j-1]))
f[i][j]=min(f[i-1][j-1],f[i-1][j],f[i][j-1])+1;
ans=std::max(f[i][j],ans);
}
if(!ans)
ans--;
//特判,原因显然是边长为1无法构成01相间的正方形
printf("%d",ans+1);
return 0;
}
P2004 领地选择
二维前缀和的应用
先求出以(1,1)为左上角每个点为右下角的矩形的和
即求二维前缀和
b[i][j]=b[i-1][j]+b[i][j-1]-b[i-1][j-1]+a[i][j];
由于正方形的性质
我们只需要枚举右下角的坐标(x,y)
f[i][j]表示以(i,j)为右下角(i-c,j-c)为左上角的正方形的和
转移方程:f[i][j]=b[i][j]-b[i-c][j]-b[i][j-c]+b[i-c][j-c]
#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=1611,inf=0x1f1f1f1f;
int f[maxn][maxn],a[maxn][maxn],b[maxn][maxn];
int lastx,lasty,n,m,c,ans=-inf;
int main()
{
scanf("%d%d%d",&n,&m,&c);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&a[i][j]),b[i][j]=a[i][j]+b[i-1][j]+b[i][j-1]-b[i-1][j-1];
for(int i=c;i<=n;i++)
for(int j=c;j<=m;j++)
{
f[i][j]=b[i][j]-b[i-c][j]-b[i][j-c]+b[i-c][j-c];
if(ans<f[i][j])
{
ans=f[i][j];
lastx=i-c,lasty=j-c;
}
}
printf("%d %d",lastx+1,lasty+1);
return 0;
}
P2072 宗教问题
这题线性结构十分明显,每次转移都要遍历寻找上一次的状态
f[i]表示前i个人至少要分成多少个集合
ff[i]表示前i个人的至少有多少点危险度
易知,每一次的转移都要寻找到上一个集合的状态
因此就有两重循环
i : 1~n 遍历所有点
j : i~1 寻找最后一个集合(也就是当前点所在的集合)的开头元素,注意当最后一个集合元素种数超过k就break
转移很简单
因为j是最后一个集合的开头,j-1就是上一个集合的结尾
f[i]=std::min(f[i],f[j-1]+1);
ff[i]=std::min(ff[i],ff[j-1]+cnt);
//其中cnt为当前集合的元素种数
边界问题
而且由于j-1会访问到0,所以0要特殊处理
#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=1101,maxm=1101,inf=0x1f1f1f1f;
int n,V,f[maxn],ff[maxn],m,k,bel[maxm],cnt,ton[maxn];
int min(int x,int y,int z)
{
return std::min(std::min(x,y),z);
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=n;i++)
scanf("%d",&bel[i]);
memset(f,0x1f,sizeof(f));
memset(ff,0x1f,sizeof(ff));
f[0]=0,ff[0]=0;
for(int i=1;i<=n;i++)
{
cnt=0;
memset(ton,false,sizeof(ton));
for(int j=i;j>=1;j--)
{
if(!ton[bel[j]])
cnt++,ton[bel[j]]=true;
if(cnt>k)
break;
f[i]=std::min(f[i],f[j-1]+1);
ff[i]=std::min(ff[i],ff[j-1]+cnt);
}
}
printf("%d\n%d",f[n],ff[n]);
return 0;
}
P1564 膜拜
宗教那道题的弱化版
开始直接把板子改了改码了上去,然后WA了0.5h…
后来当发现不满足条件时,不能直接break掉,因为有可能会反向来人导致条件又满足了,比如m=1时,当前有221,下一个是2,然而下4个是2111,这样条件又满足了
#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=2511,inf=0x1f1f1f1f;
int f[maxn],ff[maxn],ton[maxn],bel[maxn];
int n,m;
int min(int x,int y,int z)
{
return std::min(std::min(x,y),z);
}
int abs(int x)
{
return x>0?x:-x;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&bel[i]);
memset(f,0x1f,sizeof(f));
f[0]=0;
for(int i=1;i<=n;i++)
{
memset(ton,0,sizeof(ton));
for(int j=i;j>=1;j--)
{
ton[bel[j]]++;
if(abs(ton[1]-ton[2])<=m||!ton[1]||!ton[2])
f[i]=std::min(f[i],f[j-1]+1);
}
}
printf("%d",f[n]);
return 0;
}
(待处理)区间DP
P2426 删数
P2858 Treats for the cows
P1435 回文子串
P2066 机器分配
(待处理)单调队列优化DP
P1440 求m区间内的最小值
P2782 友好城市
P2627 修剪草坪
P3957 跳房子
二分答案+DP
实质上是二分答案,然后用dp检验可行性
P2370 yyy2015c01的U盘
对于大的背包来说,接口越大价值越大,满足单调性
直接二分答案+可行性01背包check是否可行
#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=1101,inf=0x1f1f1f1f;
int f[maxn],cv[maxn],w[maxn];
int n,p,V,mx=-1,ans;
int check(int mid)
{
memset(f,0,sizeof(f));
for(int i=1;i<=n;i++)
{
if(cv[i]>mid)
continue;
for(int j=V;j>=cv[i];j--)
f[j]=std::max(f[j],f[j-cv[i]]+w[i]);
}
for(int j=V;j>=1;j--)
if(f[j]>=p)
return true;
return false;
}
int main()
{
scanf("%d%d%d",&n,&p,&V);
for(int i=1;i<=n;i++)
scanf("%d%d",&cv[i],&w[i]),mx=std::max(mx,cv[i]);
int l=1,r=mx;
while(l<=r)
{
int mid=(l+r)/2;
if(check(mid))
ans=mid,
r=mid-1;
else
l=mid+1;
}
if(ans)
{
printf("%d",ans);
return 0;
}
printf("No Solution!");
return 0;
}