集训之各种dp
1.线性
「BZOJ1609」麻烦的聚餐
分别求一遍连续非下降/上升子序列长度,用总长减去,取最小值即可,主要\(O(n^2)\)优化
Code
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
const int maxn=3e5+5;
typedef long long ll;
int n,f[maxn],a[maxn],top,ans;
int main(){
// freopen("1.in","r",stdin);
scanf("%d",&n);
for(int i=1;i<=n;++i)scanf("%d",&a[i]);
f[++top]=a[1];
for(int i=2;i<=n;++i){
if(a[i]>=f[top]){
f[++top]=a[i];
continue;
}
int x=upper_bound(f+1,f+1+top,a[i])-f;
f[x]=a[i];
}
ans=n-top;
f[top=1]=a[n];
for(int i=n-1;i>=1;--i){
if(a[i]>=f[top]){
f[++top]=a[i];
continue;
}
int x=upper_bound(f+1,f+1+top,a[i])-f;
f[x]=a[i];
}
printf("%d\n",min(ans,n-top));
return 0;
}
「P2066」机器分配
\(f[i][j]\)表示i个公司分j台机器所得的最大利润
转移方程:\(f[i][j]=max(f[i][j],f[i-1][k]+a[i][j-k]),1<=i<=n,1<=j<=m,0<=k<=j\)
Code
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn=20;
int f[maxn][maxn],a[maxn][maxn],n,m,ans,xx,shu[maxn];
void B(int x,int y){
if (x==0) return;
for (int k=0;k<=y;k++){
if (ans==f[x-1][k]+a[x][y-k]){
ans=f[x-1][k];
B(x-1,k);
printf("%d %d\n",x,y-k);
break;
}
}
}
int main(){
// freopen("1.in","r",stdin);
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){
for(int k=0;k<=j;++k){
f[i][j]=max(f[i][j],f[i-1][k]+a[i][j-k]);
}
}
printf("%d\n",f[n][m]);
ans=f[n][m];
B(n,m);
return 0;
}
2.树形
没有上司的舞会
相邻节点不能在一起,0表示不参加,1表示参加。
主要要搞清0/1分别由谁转移,详见代码,不赘述。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
const int maxn=6000+5;
typedef long long ll;
struct Edge{int next,to;}e[maxn*2];
int n,a[maxn],f[maxn][2],cnt,x,y,head[maxn];
void Add(int x,int y){
e[++cnt].to=y;
e[cnt].next=head[x];
head[x]=cnt;
}
void dfs(int u,int fa){
f[u][1]=a[u];
int xx=0;
for(int i=head[u];i;i=e[i].next){
int v=e[i].to;
if(v==fa)continue;
dfs(v,u);
f[u][1]=max(f[u][1],f[v][0]+a[u]);
xx+=max(f[v][1],f[v][0]);
}
f[u][0]=xx;
}
int main(){
// freopen("1.in","r",stdin);
scanf("%d",&n);
for(int i=1;i<=n;++i)scanf("%d",&a[i]);
for(int i=1;i<n;++i)scanf("%d%d",&x,&y),Add(x,y),Add(y,x);
scanf("%d%d",&x,&y);
dfs(1,0);
printf("%d\n",max(f[1][1],f[1][0]));
return 0;
}
小胖守皇宫
题目就是说,每相邻的两个节点必须有一个人守着,并有一定的花费,给你个节点花费,求花费最小。
0表示儿子守着,1是自己守,2是父亲守。
那个tot变量有些迷,待更新吧,树形的和后面的状压还得持续更新
Code
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
const int maxn=1500+5,Inf=0x3f3f3f3f;
typedef long long ll;
struct Edge{int next,to;}e[maxn*2];
int n,a[maxn],f[maxn][3],cnt,x,m,k,head[maxn];
void Add(int x,int y){
e[++cnt].to=y;
e[cnt].next=head[x];
head[x]=cnt;
}
void dfs(int u,int fa){
f[u][1]=a[u];
int tot=Inf;
for(int i=head[u];i;i=e[i].next){
int v=e[i].to;
if(v==fa)continue;
dfs(v,u);
f[u][1]+=min(f[v][0],min(f[v][1],f[v][2]));
f[u][0]+=min(f[v][1],f[v][0]);
f[u][2]+=min(f[v][1],f[v][0]);
tot=min(tot,f[v][1]-f[v][0]);
}
if(tot>0)f[u][0]+=tot;
}
int main(){
//freopen("1.in","r",stdin);
scanf("%d",&n);
int aa;
for(int i=1;i<=n;++i){
scanf("%d%d%d",&k,&aa,&m);
a[k]=aa;
while(m--){
scanf("%d",&x);
Add(k,x);Add(x,k);
}
}
if(n==1)printf("%d\n",a[1]);
else{
dfs(1,0);
printf("%d\n",min(f[1][0],f[1][1]));
}
return 0;
}
3.区间
整数划分
和乘积最大那道题很像,不过更复杂一点,要输出。
f[i][j]表示前i位划分为m部分的最大乘积。(就这个部分关键要想到)
预处理出任意i~j位的数a[i][j]
转移:\(f[i][j]=max(f[i][j],f[k][j-1]*a[k+1][i]),j-1<=k<=i-1\)
边界:f[0][0]=1(因为是相乘,所以为1不能为0)
转移时记录前i位划分为j段的最后一个“*”前面那位数,递归输出。
Code
#include <cstring>
#include <cmath>
#include <algorithm>
#include <cstdio>
using namespace std;
typedef long long ll;
const int maxn=23;
ll a[maxn][maxn],f[maxn][maxn];
int t,hua[maxn][maxn],m,n,len;
char s[maxn];
void P(int x,int y){
if(y==0)return;
P(hua[x][y],y-1);
for(int i=hua[x][y]+1;i<=x;++i)printf("%c",s[i]);
printf(" ");
}
void C(){
memset(f,0,sizeof f);
memset(a,0,sizeof a);
memset(hua,0,sizeof hua);
memset(s,0,sizeof s);
}
void R(){
scanf(" %s %d",s+1,&m);
n=strlen(s+1),m=min(m,n);
for(int i=1;i<=n;++i)
for(int j=i;j<=n;++j) a[i][j]=a[i][j-1]*10LL+s[j]-'0';
memset(f,-1,sizeof f);
f[0][0]=1;
}
void Solve(){
for(int i=1;i<=n;++i)
for(int j=1;j<=min(i,m);++j)
for(int k=j-1;k<=i-1;++k)
if(f[i][j]<f[k][j-1]*a[k+1][i]) f[i][j]=f[k][j-1]*a[k+1][i],hua[i][j]=k;
}
void Pr(){
printf("%lld\n",f[n][m]);
P(n,m);
printf("\n");
}
int main(){//可怜的主函数
//freopen("1.in","r",stdin);
scanf("%d",&t);
while(t--){
C();R();
Solve();
Pr();
}
return 0;
}
矩阵连乘
题意解释一下:
可以这么理解:对于三个连续的矩阵,分两步求,再将这两步结果相加。
A:2 * 3 B:3 * 4 C:4 * 5
(A * B) * C=(2 * 3 * 4)+(2 * 4 * 5)
A * ( B * C)=(3 * 4 * 5)+(2 * 3 * 5)
f[i][j]表示第i个到第j个的矩阵乘积最小运算量
转移:\(f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+a[i-1]*a[k]*a[j]),i<=k<j\)
边界:\(f[i][j]=Inf,f[i][i]=0,1<=i<=n\)
Code
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
const int maxn=105,Inf=0x7f7f7f7f;
int n,f[maxn][maxn],a[maxn];
int main(){
// freopen("1.in","r",stdin);
memset(f,Inf,sizeof f);
scanf("%d",&n);
for(int i=0;i<=n;++i)scanf("%d\n",&a[i]);
for(int i=1;i<=n;++i)f[i][i]=0;
for(int l=2;l<=n;++l)
for(int i=1,j;i+l-1<=n;++i){
j=i+l-1;
for(int k=i;k<j;++k) f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+a[i-1]*a[k]*a[j]);
}
printf("%d\n",f[1][n]);
return 0;
}
凸多边形的三角剖分
也是区间的套路,由简单衍生出来
先顺时针给各个顶点编上号,\(f[i][j]\)表示连接i号顶点和j号顶点,所形成的下面的一个多边形的最小剖分值。
转移:\(f[i][j]=min(f[i][k]+f[k][j]+a[i] * a[k] * a[j]),i+1<=k<=j-1\)
边界:初始化\(f[i][i+2]=a[i] * a[i+1] * a[i+2],(1<=i<=n-2)\)
Code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=50+5;
int n;
ll a[maxn],f[maxn][maxn];
int main(){
//freopen("1.in","r",stdin);
scanf("%d",&n);
for(int i=1;i<=n;++i)scanf("%lld\n",&a[i]);
for(int i=1;i+2<=n;++i)f[i][i+2]=a[i]*a[i+1]*a[i+2];
for(int d=3;d<=n;++d){
for(int i=1;i+d-1<=n;++i){
int j=i+d-1;
for(int k=i+1;k<j;++k){
if(f[i][j])f[i][j]=min(f[i][j],f[i][k]+f[k][j]+a[k]*a[i]*a[j]);
else f[i][j]=f[i][k]+f[k][j]+a[k]*a[i]*a[j];
}
}
}
printf("%lld\n",f[1][n]);
return 0;
}
多边形
李煜东的《算法进阶指南》P284~286
我就不再解释啦
Code
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <iostream>
#include <cstring>
using namespace std;
const int maxn=50+5,Inf=1e9+10;
int n,data[maxn<<1];
int f[maxn<<1][maxn<<1][3];
int ff[maxn<<1];
char c[maxn<<1];
int q,w,e,r;
int ans_point=(-1)*Inf;
void Read(){
cin>>n;
for(int i=1;i<=n;++i){
cin>>c[i]>>data[i];
c[i+n]=c[i];
data[i+n]=data[i];
}
}
void Solve(){
for(int i=1;i<=n;++i){
for(int j=1;j<=(n<<1);++j){
for(int k=1;k<=(n<<1);++k){
f[j][k][1]=(-1)*Inf;
f[j][k][2]=Inf;
}
}
for(int j=1;j<=(n<<1);++j){
f[j][j][1]=f[j][j][2]=data[j];
if(c[j+1]=='t') f[j][j+1][1]=f[j][j+1][2]=data[j]+data[j+1];
else f[j][j+1][1]=f[j][j+1][2]=data[j]*data[j+1];
}
for(int l=2;l<=n;++l){
for(int j=i;j+l<=i+n;++j){
int k=j+l-1;
for(int x=j;x<k;++x){
if(c[x+1]=='t'){
f[j][k][1]=max(f[j][k][1],f[j][x][1]+f[x+1][k][1]);
f[j][k][2]=min(f[j][k][2],f[j][x][2]+f[x+1][k][2]);
}else{
q=f[j][x][1]*f[x+1][k][1];
w=f[j][x][2]*f[x+1][k][2];
e=f[j][x][1]*f[x+1][k][2];
r=f[j][x][2]*f[x+1][k][1];
f[j][k][1]=max(f[j][k][1],max(max(q,w),max(e,r)));
f[j][k][2]=min(f[j][k][2],min(min(q,w),min(e,r)));
}
}
}
}
ff[i]=f[i][i+n-1][1];
}
for(int i=1;i<=n;++i){
ans_point=max(ans_point,ff[i]);
}
printf("%d\n",ans_point);
for(int i=1;i<=n;++i){
if(ans_point==ff[i]) printf("%d ",i);
}
}
int main(){
// freopen("1.in","r",stdin);
Read();
Solve();
return 0;
}
低价回文
Code
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
const int maxn=2e3+5;
int f[maxn][maxn],n,m,v[30];
char s[maxn],ss;
int main(){
// freopen("1.in","r",stdin);
scanf("%d%d %s",&n,&m,s+1);
for(int i=1,x,y;i<=m;++i)scanf(" %c%d%d",&ss,&x,&y),v[ss-'a']=min(x,y);
for(int i=m;i>=1;--i)
for(int j=i+1;j<=m;++j){
if(s[i]==s[j])f[i][j]=f[i+1][j-1];
else f[i][j]=min(f[i+1][j]+v[s[i]-'a'],f[i][j-1]+v[s[j]-'a']);
}
printf("%d\n",f[1][m]);
return 0;
}
4.状压
终于到了状压dp了,说实话我的状压dp真的不好,前几天集训时遇到状压的题我都不会,前两天没有时间,今天不能再拖了,总结一下。
互不侵犯
在\(N * N\)的棋盘里面放\(K\)个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共\(8\)个格子。
给你N和K的值,求方案数
\(1<=N<=9,0<=K<=N * N\).
我们用\(f[i][S][k]\)表示前i行放k个国王,且第i行状态为S时的方案数。状态0是不放国王,1是放国王。
边界是\(f[0][0][0]=1\),这样才能第一层转移到底层时为1(也可以直接将第一整行全预处理出来,方案数都为1)
转移是比较简单的,不需要常数了:\(f[i][S][k]+=f[i-1][s][k-cnt]\),(本行由上一行转移过来)这个k是S中1的个数,不一定是总数K,s是上一行的状态,与S不能冲突,(不能互相攻击)。
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn=1<<9;
int n,m;
ll f[10][maxn][82];
int lowbit(int x){return x&-x;}
int Q(int x){//求x的二进制中1的个数
int cnt=0;
for(int i=x;i;i-=lowbit(i))cnt++;
return cnt;
}
int main(){
scanf("%d%d",&n,&m);
f[0][0][0]=1;
int maxs=1<<n;
for(int i=1;i<=n;++i){
for(int S=0;S<maxs;++S){
int cnt=Q(S);
if(S&(S<<1))continue;
for(int s=0;s<maxs;++s){//s是上一行的状态枚举,与内层的k的枚举可以互换,但这样更省时间,因为特判会省去一些冗余循环
if(S&s||(S<<1)&s||(S>>1)&s)continue;
for(int k=cnt;k<=m;++k) f[i][S][k]+=f[i-1][s][k-cnt];
}
}
}
ll ans=0;
for(int S=0;S<maxs;++S)ans+=f[n][S][m];//最后结果是最后一行放k个国王的各种状态所对应的方案数之和
printf("%lld\n",ans);
return 0;
}
炮兵阵地
这个题目和上个差不多,除了不能相互攻击外,还有些地方不能放,最后问你的是最多的个数而不是方案数了。
\(f[i][S][s]\)是前\(i\)行中,第\(i\)行状态是\(S\),\(i-1\)行状态是\(s\)时的个数。
转移是枚举本行状态\(S\),算出该状态下本行的个数\(cnt\),用上一行的\(f[i-1][s][ss]\)再加上\(cnt\),得到\(i\)行状态为\(S\),上一行状态为\(s\)时的最大个数。
即:\(f[[i][S][s]=max(f[i][S][s],f[i-1][s][ss]+cnt)\).
其中\(ss\)是&i-2&行的状态,也需要枚举。另外本题有个优化,因为炮兵左右攻击范围挺大(2格),所以,尽管一行有M<=10个格子,但真正合法的状态数不超过60个,我们可以提前将1行的合法的状态编上号预处理出来,时间/空间复杂度都可减少,空间可减少大概八九十倍的样子。
#include <cstdio>
#include <algorithm>
using namespace std;
int n,m,tot;
const int maxn=1<<9;
int a[101],st[70],f[101][70][70];
char s[15];
bool J(int x){
if(x&(x<<1)||x&(x<<2))return 0;
return 1;
}
int Q(int x){
int cnt=0;
for(int i=st[x];i;i-=(i&-i))cnt++;
return cnt;
}
int main(){
scanf("%d%d", &n, &m);
for(int i=1; i<=n; i++){
scanf(" %s",s);
for(int j=0; j<m; j++){
if(s[j] == 'H') a[i]|=(1<<j);
}
}
int maxs=1<<m;
for(int i=0;i<maxs;++i)if(J(i))st[++tot]=i;
for(int i=1;i<=tot;++i)if(!(st[i]&a[1]))f[1][i][1]=Q(i);
for(int i=2;i<=n;++i){
for(int S=1;S<=tot;++S){
if(st[S]&a[i])continue;
int cnt=Q(S);
for(int s=1;s<=tot;++s){
if((st[S]&st[s])||(st[s]&a[i-1]))continue;
for(int ss=1;ss<=tot;++ss){
if((st[ss]&a[i-2])||(st[ss]&st[s])||(st[S]&st[ss]))continue;
f[i][S][s]=max(f[i][S][s],f[i-1][s][ss]+cnt);
}
}
}
}
int Max=0;
for(int i=1;i<=tot;++i)
for(int j=1;j<=tot;++j){
if((st[i]&a[n])||(st[j]&a[n-1])||(st[i]&st[j]))continue;
Max=max(Max,f[n][i][j]);
}
printf("%d\n",Max);
return 0;
}
旅游景点 Tourist Attractions
愤怒的小鸟
动物园
待更新吧......