动规A—线性dp
A.照相排列
AcWing271
\(1≤k≤5\),学生总人数不超过 30 人。
每排人数最多30,最多5排(数据范围较小,可以考虑多维枚举)
5维dp,5层循环枚举所有转移状态,对每一层能转移的进行累加转移
初值:\(f[0][0][0][0][0]=1\)
答案即为排满时的方案数
#include <bits/stdc++.h>
#define per(i,a,b) for(int i(a);i<=b;++i)
using namespace std;
const int N=31;
int a[10];
long long f[N][N][N][N][N];
signed main()
{
int k;
cin>>k;
while(k)
{
memset(a,0,sizeof(a));
memset(f,0,sizeof(f));
per(i,1,k) scanf("%d",a+i);
f[0][0][0][0][0]=1;//i j p q e-->5 4 3 2 1
per(i,0,a[1]) per(j,0,a[2])
{
per(p,0,a[3]) per(q,0,a[4]) per(e,0,a[5])
{
if(i<a[1]) f[i+1][j][p][q][e]+=f[i][j][p][q][e];
if(j<a[2]&&i>j) f[i][j+1][p][q][e]+=f[i][j][p][q][e];
if(p<a[3]&&j>p) f[i][j][p+1][q][e]+=f[i][j][p][q][e];
if(q<a[4]&&p>q) f[i][j][p][q+1][e]+=f[i][j][p][q][e];
if(e<a[5]&&q>e) f[i][j][p][q][e+1]+=f[i][j][p][q][e];
}
}
printf("%lld\n",f[a[1]][a[2]][a[3]][a[4]][a[5]]);
scanf("%d",&k);
}
return 0;
}
B. 最长公共上升子序列
给定两个序列,求这两个序列的最长公共上升子序列。
\(1≤n,m,a_i,b_i≤2000\)
两层循环枚举所有匹配方式
#include <bits/stdc++.h>
#define per(i,a,b) for(int i(a);i<=b;++i)
using namespace std;
const int N=2010;
int a[N],b[N],dp[N][N];
signed main()
{
int n,m;
cin>>n;
per(i,1,n) scanf("%d",a+i);
cin>>m;
per(i,1,m) scanf("%d",b+i);
per(i,1,n) per(j,1,m)//从每一个a里的数字去b里匹配
{
dp[i][j]=dp[i][j-1];//从上一个状态递推
if(a[i]==b[j]) per(k,0,i)//有公共的,向前找最长公共上升
{
if(a[i]>a[k]) dp[i][j]=max(dp[i][j],dp[k][j-1]+1);
}
}
int ans=0;
per(i,1,n) ans=max(ans,dp[i][m]);//a的各个位置的max
printf("%d\n",ans);
return 0;
}
C. 分级
poj3666 AcWing273
给定长度为 N 的序列 A,构造一个长度为 N 的序列 B,满足:
B 非严格单调,即\(B_1≤B_2≤…≤B_N\) 或\(B_1≥B_2≥…≥B_N\)。
\(最小化 S=\sum\limits_{i=1}^N∣Ai−Bi∣。\)
求最小的S
一个性质:一定存在一组最优解\(b[i]\),使得每个\(b[i]\)都是原序列中\(a[i]\)的值。
#include <bits/stdc++.h>
#define per(i,a,b) for(int i(a);i<=b;++i)
using namespace std;
const int N=2010;
int a[N],aa[N],f[N][N];
bool cmp(int f,int s){ return f>s;}
signed main()
{
int n,ans=0x3f3f3f3f;
cin>>n;
per(i,1,n) scanf("%d",a+i),aa[i]=a[i];
sort(aa+1,aa+n+1);//正序
memset(f,0x3f,sizeof(f));
per(i,1,n) f[0][i]=0;
per(i,1,n) per(j,1,n) f[i][j]=min(f[i][j-1],f[i-1][j]+abs(a[i]-aa[j]));
//比较a[i]取哪个aa[i]结果最小(保证单调性)
per(i,1,n) ans=min(ans,f[n][i]);
memset(f,0x3f,sizeof(f));
per(i,1,n) f[0][i]=0;
reverse(aa+1,aa+n+1);//逆序
per(i,1,n) per(j,1,n) f[i][j]=min(f[i][j-1],f[i-1][j]+abs(a[i]-aa[j]));
//比较a[i]取哪个aa[i]结果最小(保证单调性)
per(i,1,n) ans=min(ans,f[n][i]);
printf("%d\n",ans);
return 0;
}
D. 移动服务
AcWing274
一个公司有三个移动服务员,最初分别在位置1,2,3处。
如果某个位置(用一个整数表示)有一个请求,那么公司必须指派某名员工赶到那个地方去。
某一时刻只有一个员工能移动,且不允许在同样的位置出现两个员工。
从 p到 q 移动一个员工,需要花费 \(c(p,q)\)。
这个函数不一定对称,但保证\(c(p,p)=0\)。
给出 N 个请求,请求发生的位置分别为 \(p_1∼p_N\)。
公司必须按顺序依次满足所有请求,且过程中不能去其他额外的位置,目标是最小化公司花费,请你帮忙计算这个最小花费。
#include <bits/stdc++.h>
#define per(i,a,b) for(int i(a);i<=b;++i)
using namespace std;
const int N=210,M=1010;
int a[N][N],p[M],f[2][N][N];
//p 存请求,f为压维dp,后两维是两个服务员的位置,第三个的位置由p可推出
signed main()
{
int z,v,u,n,m;
cin>>n>>m;
per(i,1,n) per(j,1,n) scanf("%d",&a[i][j]);
per(i,1,m) scanf("%d",p+i);
p[0]=3;
memset(f,0x3f,sizeof(f));
per(i,0,m-1)
{
memset(f[(i+1)%2],0x3f,sizeof(f[(i+1)%2]));
per(x,1,n) per(y,1,n)
{
z=p[i],v=f[i%2][x][y];//z:第三个服务员位置,v:枚举每一个转移来的状态
if(i==0&&x==1&&y==2) v=0;//初值
if(x==y||y==z||x==z) continue;
u=p[i+1];//转移
f[(i+1)%2][x][y]=min(f[(i+1)%2][x][y],v+a[z][u]);
f[(i+1)%2][x][z]=min(f[(i+1)%2][x][z],v+a[y][u]);
f[(i+1)%2][z][y]=min(f[(i+1)%2][z][y],v+a[x][u]);
}
}
int ans=INT_MAX;
per(i,1,n) per(j,1,n)
{
z=p[m];
if(i==j||j==z||i==z) continue;
ans=min(ans,f[m%2][i][j]);//取最小值
}
printf("%d\n",ans);
return 0;
}
E. I-区域
在 \(N×M\) 的矩阵中,每个格子有一个权值,要求寻找一个包含 K 个格子的凸连通块(连通块中间没有空缺,并且轮廓是凸的),使这个连通块中的格子的权值和最大。
注意:凸连通块是指:连续的若干行,每行的左端点列号先递减、后递增,右端点列号先递增、后递减。
求出这个最大的权值和,并给出连通块的具体方案,输出任意一种方案即可。
状态表示:\(f[i][j][l][r][x][y]\),表示当前处理到了第i行,已经选出了j个格子,第i行的选择是从第l列到第r列,x,y表示当前行的左右端点是往外伸还是往里面缩,我们规定端点的列坐标比上一行的列坐标小用1表示,大了用0表示,所以左端点往外伸/缩的状态对应x=1/0,右端点往外伸/缩的状态对应y=0/1。
#include <bits/stdc++.h>
#define per(i,a,b) for(int i(a);i<=b;++i)
using namespace std;
const int N=16;
int a[N][N];
int f[N][N*N][N][N][2][2];//左:扩/缩-1/0,右:0/1
struct aaa{
int i,k,l,r,x,y;
}g[N][N*N][N][N][2][2],e;
signed main()
{
int n,m,k,val;
cin>>n>>m>>k;
per(i,1,n) per(j,1,m) scanf("%d",&a[i][j]);
memset(f,-0x3f3f3f3f,sizeof(f));
per(i,1,n) per(j,0,k)
{
per(l,1,m) per(r,1,m)
{
if(j<r-l+1) continue;
//左扩,右扩
{
auto &vf=f[i][j][l][r][1][0];
auto &vg=g[i][j][l][r][1][0];
if(j==r-l+1) vf=0;
per(p,l,r) per(q,p,r)
{
val=f[i-1][j-(r-l+1)][p][q][1][0];
if(val>vf)
{
vf=val;
vg={i-1,j-(r-l+1),p,q,1,0};
}
}
per(u,l,r) vf+=a[i][u];
}
//左扩,右缩
{
auto &vf=f[i][j][l][r][1][1];
auto &vg=g[i][j][l][r][1][1];
if(j==r-l+1) vf=0;
per(p,l,r) per(q,r,m) per(y,0,1)
{
val=f[i-1][j-(r-l+1)][p][q][1][y];
if(val>vf)
{
vf=val;
vg={i-1,j-(r-l+1),p,q,1,y};
}
}
per(u,l,r) vf+=a[i][u];
}
//左缩,右扩
{
auto &vf=f[i][j][l][r][0][0];
auto &vg=g[i][j][l][r][0][0];
// if(j==r-l+1) vf=0;
per(p,1,l) per(q,l,r) per(x,0,1)
{
val=f[i-1][j-(r-l+1)][p][q][x][0];
if(val>vf)
{
vf=val;
vg={i-1,j-(r-l+1),p,q,x,0};
}
}
per(u,l,r) vf+=a[i][u];
}
//左缩,右缩
{
auto &vf=f[i][j][l][r][0][1];
auto &vg=g[i][j][l][r][0][1];
// if(j==r-l+1) vf=0;
per(p,1,l) per(q,r,m) per(x,0,1) per(y,0,1)
{
val=f[i-1][j-(r-l+1)][p][q][x][y];
if(val>vf)
{
vf=val;
vg={i-1,j-(r-l+1),p,q,x,y};
}
}
per(u,l,r) vf+=a[i][u];
}
}
}
int ans=0;
per(i,1,n) per(l,1,m) per(r,1,m) per(x,0,1) per(y,0,1)
{
val=f[i][k][l][r][x][y];
if(val>ans)
{
ans=val;
e={i,k,l,r,x,y};
}
}
printf("%d\n",ans);
if(!ans) return 0;
while(e.k)
{
per(i,e.l,e.r) printf("%d %d\n",e.i,i);
e=g[e.i][e.k][e.l][e.r][e.x][e.y];
}
return 0;
}
F. 饼干
圣诞老人共有 \(M\)个饼干,准备全部分给 \(N\)个孩子。
每个孩子有一个贪婪度,第i个孩子的贪婪度为 \(g[i]\)。
如果有 \(a[i]\) 个孩子拿到的饼干数比第 i 个孩子多,那么第 i 个孩子会产生 \(g[i]×a[i]\) 的怨气。
给定 N、M和序列 g,圣诞老人请你帮他安排一种分配方式,使得每个孩子至少分到一块饼干,并且有孩子的怨气总和最小。
#include <bits/stdc++.h>
#define per(i,a,b) for(int i(a);i<=b;++i)
using namespace std;
int g[35],f[35][5010],s[35];//f[i][j]前i个小孩分到j个饼干的怨气值
signed main()
{
int n,m;
cin>>n>>m;
per(i,1,n) scanf("%d",g+i);
sort(g+1,g+n+1);
reverse(g+1,g+n+1);//从大到小排序
memset(f,0x3f,sizeof(f));
f[0][0]=0;
per(i,1,n)//怨气值从大到小,枚举小孩
{
s[i]=s[i-1]+g[i];
per(j,i,m)//枚举饼干数
{
f[i][j]=min(f[i][j],f[i][j-i]);//每个小孩多给一块 j-i
per(k,0,i-1) f[i][j]=min(f[i][j],f[k][j-i+k]+k*(s[i]-s[k]));
}
}
printf("%d\n",f[n][m]);
return 0;
}
G. 花店橱窗
n行m列的矩阵,要求每行选一个数,且该数的列数>上一行选的数
且要求输出选择的方案
二维dp,转移最大值(\(f\))的同时记录从哪转移来的(\(p\))
#include <bits/stdc++.h>
#define per(i,a,b) for(int i(a);i<=b;++i)
using namespace std;
const int N=110;
int a[N][N],f[N][N],p[N][N],st[N];
signed main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
int n,m;
cin>>n>>m;
per(i,1,n) per(j,1,m) scanf("%d",&a[i][j]);
memset(f,-0x3f,sizeof(f));
per(i,0,m) f[0][i]=0;
per(i,0,n) f[i][0]=0;//初始化
per(i,1,n) per(j,i,m)
{
if(j!=i)//有选择
{
if(f[i][j-1]>=f[i-1][j-1]+a[i][j])//选前面的数比当前优
{
f[i][j]=f[i][j-1];
p[i][j]=p[i][j-1];
}
else//当前更优
{
f[i][j]=f[i-1][j-1]+a[i][j];
p[i][j]=j;
}
}
else //只能选当前
{
f[i][j]=f[i-1][j-1]+a[i][j];
p[i][j]=j;
}
}
int ans=-INT_MAX,nw=0,top=0;
per(i,n,m) if(ans<f[n][i]) ans=f[n][i],nw=i;
while(n)//从后往前找,入栈
{
st[++top]=nw;
nw=p[--n][nw-1];
}
printf("%d\n",ans);
for(int i=top;i>=1;--i) printf("%d ",st[i]);//出栈
return 0;
}
H. 低买
求一个序列的最长单调递减序列的长度及个数
n<5000
\(n^2\)求最长单调序列,加统计方案
#include <bits/stdc++.h>
#define per(i,a,b) for(int i(a);i<=b;++i)
using namespace std;
const int N=5010;
int a[N],f[N],dp[N];
signed main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
int n,maxn=0,ans=0;
cin>>n;
per(i,1,n) scanf("%d",a+i);
per(i,1,n)
{
f[i]=1;
per(j,1,i-1)
{
if(a[j]>a[i]) f[i]=max(f[i],f[j]+1);//暴力统计
}
per(j,1,i-1)
{
if(f[i]==f[j]&&a[i]==a[j]) dp[j]=0;//更新清零
else if(f[i]==f[j]+1&&a[j]>a[i]) dp[i]+=dp[j];//寻找转移,方案累加
}
maxn=max(maxn,f[i]);
if(f[i]==1) dp[i]=1;//赋初始值
}
per(i,1,n) if(f[i]==maxn) ans+=dp[i];
printf("%d %d\n",maxn,ans);
return 0;
}
I. 旅行
两个序列,求他们的最长不连续公共序列
二维枚举,刷表,深搜输出
#include <bits/stdc++.h>
#define per(i,a,b) for(int i(a);i<=b;++i)
using namespace std;
const int N=90;
char a[N],b[N],f[N][N],path[N],n,m;
void sol(int i,int j,int u,int len)
{
if(u>len)
{
puts(path+1);
return;
}
int x,y;
if(a[i]==b[j])
{
path[u]=a[i];
sol(i+1,j+1,u+1,len);//记录,找下一个
}
else per(k,0,25)//按字典序找
{
x=y=0;
per(p,i,n) if(a[p]==k+'a')
{
x=p;
break;
}
per(p,j,m) if(b[p]==k+'a')
{
y=p;
break;
}
if(x&&y&&f[x][y]==f[i][j]) sol(x,y,u,len);
}
}
signed main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
cin>>a+1>>b+1;
n=strlen(a+1),m=strlen(b+1);
for(int i=n;i>=1;--i) for(int j=m;j>=1;--j)
{
if(a[i]!=b[j]) f[i][j]=max(f[i+1][j],f[i][j+1]);
else f[i][j]=f[i+1][j+1]+1;//求最长公共序列
}
sol(1,1,1,f[1][1]);
return 0;
}
J. 减操作
定义数组第 i 位上的减操作:把 $a_i $和 \(a_{i+1}\) 换成 \(a_i−a_{i+1}\)。
con([12,10,4,3,5],2)=[12,6,3,5]
con([12,6,3,5],3)=[12,6,−2]
con([12,6,−2],2)=[12,8]
con([12,8],1)=[4]
问题转化为先求出各个位置的符号,再构造con
#include <bits/stdc++.h>
#define per(i,a,b) for(int i(a);i<=b;++i)
using namespace std;
const int N=110,M=10010,base=10000;
int a[N],f[N][M*2],ans[N];
signed main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
int n,t;
cin>>n>>t;
per(i,1,n) scanf("%d",a+i);
f[1][a[1]+base]=1,f[2][a[1]-a[2]+base]=-1;//a_2一定是 -
per(i,3,n) per(j,-10000+base,10000+base) //dp求符号
{
if(f[i-1][j])
{
f[i][j-a[i]]=-1;
f[i][j+a[i]]=1;
}
}
int v=t+base;
int x=n;
while(x>1)
{
ans[x]=f[x][v];//逆推,找出每个位置对应符号
if(ans[x]==1) v-=a[x];
else v+=a[x];
x--;
}
int cnt=0;
per(i,2,n)
{
if(ans[i]==1)//为“+” 先执行 con
{
printf("%d ",i-cnt-1);
++cnt;
}
}
per(i,2,n) if(ans[i]==-1) printf("1 ");//对剩余“ - ”进行con
return 0;
}
K. 传纸条
一个矩阵,找两条不重叠路线,使其权值和最大
四维矩阵dp,分别代表两个点的位置,分别有四种转移情况
\(f[i][j][k][l]= max( f[i-1][j][k-1][l], f[i-1][j][k][l-1], f[i][j-1][k-1][l],f[i][j-1][k][l-1])\)
特判重合
#include <bits/stdc++.h>
#define per(i,a,b) for(int i(a);i<=b;++i)
using namespace std;
const int N=55;
int a[N][N],f[N][N][N][N];
signed main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
int n,m;
cin>>n>>m;
per(i,1,n) per(j,1,m) scanf("%d",&a[i][j]);
per(i,1,n) per(j,1,m) per(k,1,n) per(l,1,m)
{
if(i==k&&j==l) f[i][j][k][l]=-100;
else f[i][j][k][l]=max(max(f[i-1][j][k-1][l],f[i][j-1][k-1][l]),max(f[i-1][j][k][l-1],f[i][j-1][k][l-1]))+a[i][j]+a[k][l];
}
printf("%d\n",max(f[n-1][m][n][m-1],f[n][m-1][n-1][m]));
return 0;
}
L. 乌龟棋
一行n个格子,每个格子有权值,有4种卡片,每次分别可以走1,2,3,4步,每跳一次,能拿取落点的权值,问怎么走才能拿到最大权值
输入的步数总和=n
注意处理越界状态
#include <bits/stdc++.h>
#define per(i,a,b) for(int i(a);i<=b;++i)
using namespace std;
const int N=360,M=45;
int a[N],b[5],f[M][M][M][M];
int sol(int i,int j,int k,int l){ return a[i+j*2+k*3+l*4];}
signed main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
int n,m,x;
cin>>n>>m;
per(i,0,n-1) scanf("%d",a+i);
per(i,1,m) scanf("%d",&x),b[x]++;
f[0][0][0][0]=a[0];
per(i,0,b[1]) per(j,0,b[2]) per(k,0,b[3]) per(l,0,b[4])//表示每张牌分别用了几次
{
x=sol(i,j,k,l);
int &t=f[i][j][k][l];
if(i) t=max(t,f[i-1][j][k][l]+x);
if(j) t=max(t,f[i][j-1][k][l]+x);
if(k) t=max(t,f[i][j][k-1][l]+x);
if(l) t=max(t,f[i][j][k][l-1]+x);
}
printf("%d\n",f[b[1]][b[2]][b[3]][b[4]]);
return 0;
}