啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊我dp菜死了快来补一下。基本没讲解,来就放代码,偶尔写两句,也是在瞎扯。
简介
动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。
线性DP
LIS
$O(nlong)$的方法用二分优化。
#include<bits/stdc++.h> using namespace std; int n,a[100005],f[100005],cnt; int main() { ios::sync_with_stdio(false); cin>>n; for(int i=1;i<=n;i++)cin>>a[i]; for(int i=1;i<=n;i++) { if(f[cnt]<a[i])f[++cnt]=a[i]; else { int mid=upper_bound(f+1,f+1+cnt,a[i])-f; f[mid]=a[i]; } } cout<<cnt<<" "; return 0; }
LCS
困死我了。
#include<bits/stdc++.h> using namespace std; int f[5005][5005],n; char a[5005],b[5005]; int main() { ios::sync_with_stdio(false); // cin>>n; scanf("%s",a); scanf("%s",b); int la=strlen(a),lb=strlen(b); for(int i=1;i<=la;i++) for(int j=1;j<=lb;j++) { if(a[i-1]==b[j-1])f[i][j]=max(f[i-1][j-1]+1,f[i][j]); f[i][j]=max(f[i][j],max(f[i-1][j],f[i][j-1])); } cout<<f[la][lb]<<" "; return 0; }
LCIS
我真的好困。。
#include<bits/stdc++.h> using namespace std; int n,a[5005],b[5005],f[5005][5005]; int main() { ios::sync_with_stdio(false); cin>>n; for(int i=1;i<=n;i++)cin>>a[i]; for(int j=1;j<=n;j++)cin>>b[j]; for(int i=1;i<=n;i++) { int val=0; if(b[0]<a[i])val=f[i-1][0]; for(int j=1;j<=n;j++) { if(a[i]==b[j])f[i][j]=val+1; else f[i][j]=f[i-1][j]; if(b[j]<a[i])val=max(val,f[i-1][j]); } } int maxn=0; for(int i=1;i<=n;i++)maxn=max(maxn,f[n][i]); cout<<maxn<<" "; return 0; }
背包问题
背包是一类经典dp。
01背包
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
关于初始化:
如果要求了把背包放满,那么初始化f[0]=0,f[1~n]=-INF。
如果没有要求放满,初始化为0。
所有的背包问题基本都这样初始化。
我们把j从m循环到w[i]:
1.寻找放了w[i]后剩余空间的最大值。
2.从大到小循环,保证当前状态的上一个状态不会出现已有当前物品的情况。
#include<cstdio> #include<algorithm> using namespace std; int n,m,w[205],c[205],f[205]; int main() { scanf("%d%d",&m,&n); for(int i=1;i<=n;i++) scanf("%d%d",&w[i],&c[i]); for(int i=1;i<=n;i++) for(int j=m;j>=w[i];j--) f[j]=max(f[j],f[j-w[i]]+c[i]); printf("%d ",f[m]); return 0; }
完全背包
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
和0~1背包的唯一区别就是有可能一个物品放多种。
反正是同样的背包,你换一换第二维循环顺序不就行了吗?这样其实就是从绝不可能重复到可能重复,刚好满足完全背包的要求,理解清了0/1的循环顺序,这个也就没问题了。
#include<cstdio> #include<algorithm> using namespace std; int n,m,w[205],c[205],f[205]; int main() { scanf("%d%d",&m,&n); for(int i=1;i<=n;i++) scanf("%d%d",&w[i],&c[i]); for(int i=1;i<=n;i++) for(int j=w[i];j<=m;j++) f[j]=max(f[j],f[j-w[i]]+c[i]); printf("max=%d ",f[m]); return 0; }
多重背包
有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
这个题可以用二进制拆分思想来做,把每一个n[i]的系数化为1,2,4,...,2^(k-1),n[i]-2^k+1。
假如有一个n[i]=13,那么13=(2^0)+(2^1)+(2^2)+13-(2^3)+1,k在此处为3。
这样可以保证0~n[i]区间的每一个整数都可以用这些系数相加得到。
所以该问题能转化为0/1背包。
注意处理n-(2^k)+1。
多重背包复杂度O(vΣNi=1logn[i])
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; int n,type,m,v,wt,num; int T; struct node { int w=0,c=0; }; int f[1005]; int main() { scanf("%d",&T); while(T--) { n=0; node a[1005]; scanf("%d%d",&m,&type); while(type--) { scanf("%d%d%d",&v,&wt,&num); int p=1; while(num-p>0) { num-=p; a[++n].w=v*p; a[n].c=wt*p; p<<=1; } a[++n].w=num*v; a[n].c=wt*num; //注意要处理n[i]-(2^k)+1这个情况 } memset(f,0,sizeof(f)); for(int i=1;i<=n;i++) for(int j=m;j>=a[i].w;j--) f[j]=max(f[j],f[j-a[i].w]+a[i].c); printf("%d ",f[m]); } return 0; }
单调队列优化多重背包以后再写。
混合背包
就是把01,完全,多重这三种背包混合在一起。
思路:
for i=1..N
if 第i件物品是01背包
ZeroOnePack(c[i],w[i])
else if 第i件物品是完全背包
CompletePack(c[i],w[i])
else if 第i件物品是多重背包
MultiplePack(c[i],w[i],n[i])
#include<cstdio> #include<algorithm> using namespace std; int n,m,f[205],cnt; struct node { int w,c,flag; }a[205]; int main() { scanf("%d%d",&m,&n); while(n--) { int ww,cc,p; scanf("%d%d%d",&ww,&cc,&p); if(p==0){a[++cnt].w=ww,a[cnt].c=cc;continue;} else { int mul=1; while(p-mul>0) { p-=mul; a[++cnt].c=mul*cc; a[cnt].w=mul*ww; a[cnt].flag=1; mul<<=1; } a[++cnt].c=p*cc; a[cnt].w=p*ww; a[cnt].flag=1; } } for(int i=1;i<=cnt;i++) { if(a[i].flag) for(int j=m;j>=a[i].w;j--) f[j]=max(f[j],f[j-a[i].w]+a[i].c); else for(int j=a[i].w;j<=m;j++) f[j]=max(f[j],f[j-a[i].w]+a[i].c); } printf("%d ",f[m]); return 0; }
看到这里累了吗?我也好累,不过挺开心的。qwq
二维费用背包
就是说有两种背包容量,两种体积(咋好理解咋来ber),我这里给出01二维背包,正常人都会推广到二维费用完全、多重、混合。
#include<cstdio> #include<algorithm> using namespace std; int n,M,T,m[205],t[205],f[205][205]; int main() { scanf("%d%d%d",&n,&M,&T); for(int i=1;i<=n;i++) scanf("%d%d",&m[i],&t[i]); for(int i=1;i<=n;i++) for(int j=M;j>=m[i];j--) for(int k=T;k>=t[i];k--) f[j][k]=max(f[j][k],f[j-m[i]][k-t[i]]+1); printf("%d ",f[M][T]); return 0; }
分组背包
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
其实就是01背包上面加了一维,假设划分为m组,每一组只能选一个,那么在01基础上套一维就行,不过要注意每一维的顺序,这很重要。如果顺序无法独立写正确,那么就代表没有理解分组背包的思想。
最外层的i好理解。
j也很好理解,把j放在cnt[i]的外面,其原因是每一个j只能由1~cnt[i]中的一个物品转移而来,最终会保留j在1~cnt[i]中的最优解,也就是说针对第i组,每一个j我们只选出一个k作为最优解,然后去循环下一组。
#include<cstdio> #include<algorithm> using namespace std; int n,m,cnt[205],T,f[205]; struct node { int w,c; }a[15][35]; int main() { scanf("%d%d%d",&m,&n,&T); while(n--) { int ww,cc,p; scanf("%d%d%d",&ww,&cc,&p); a[p][++cnt[p]].w=ww; a[p][cnt[p]].c=cc; } for(int i=1;i<=T;i++) for(int j=m;j>=0;j--) for(int k=1;k<=cnt[i];k++) if(j-a[i][k].w>=0)f[j]=max(f[j],f[j-a[i][k].w]+a[i][k].c); printf("%d ",f[m]); return 0; }
背包问题的变形
在背包问题中,很多时候不只是要求最大价值,还有很多有趣的变形。
输出方案
平时我们基本不会求方案数,但实际上方案在我们求解的过程中就已经体现了出来,以01背包举例。
$f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}$,那么我们可以设置一个数组$g[i][v]$,若$f[i][v]=f[i-1][v]$,那么$g[i][v]=0$,否则$g[i][v]=1$,也就是表示第$i$项选或不选,最后输出。
伪代码如下
i=N v=V while(i>0) if(g[i][v]==0) print "未选第i项物品" else if(g[i][v]==1) print "选了第i项物品" v=v-c[i]
输出字典序最小的最优方案
这个我也不怎么理解,我也懒得在背包上面耗费过多时间。
简单来说,逆序排序物品,再正常跑方案数即可。
注意一个特殊情况,如果$f[i][v]=f[i-1][v]$和$f[i][v]=f[i-1][v-w[i]]+c[i]$是同时成立的,我们把后者的贡献计入答案而不是前者。
方案总数
在我们已经得到了诸多条件的时候,我们不仅可以求出最大贡献,还可以求出把背包装满或者装至某个容量的方案数,只需要把$max$改成$sum$。
$f[i][v]=f[i-1][v]+f[i-1][v-w[i]]$
最优方案总数
不同于方案总数,我们加上了“最优”的限制条件,很明显我们要考虑价值,那么分类讨论。
如果$f[i][j]=f[i-1][j]$,那么$g[i][j]=g[i-1][j]$
如果在当前状态下$f[i][j]=f[i-1][j-w[i]]+c[i]$,那么$g[i][j]=g[i-1][j-w[i]]$。
如果在当前状态下$f[i][j]=f[i-1][j]=f[i-1][j-w[i]]+c[i]$,那么$g[i][j]=g[i-1][j]+g[i-1][j-w[i]]$。
求第k优解
在最优解的基础上多了一个$K$的复杂度,只需要把原来的解排序(最优解到最不优解)然后统计,最后求出来就行了。
初始化?可以不写。
#include<bits/stdc++.h> using namespace std; int T,N,V,K,f[1005][35],c[1005],v[1005],tmp1[1005],tmp2[1005]; int main() { scanf("%d",&T); while(T--) { memset(f,0,sizeof f); memset(tmp1,0,sizeof tmp1); memset(tmp2,0,sizeof tmp2); scanf("%d%d%d",&N,&V,&K); for(int i=1;i<=N;i++)scanf("%d",&c[i]); for(int i=1;i<=N;i++)scanf("%d",&v[i]); for(int i=1;i<=N;i++) for(int j=V;j>=v[i];j--) { for(int k=1;k<=K;k++) tmp1[k]=f[j][k],tmp2[k]=f[j-v[i]][k]+c[i]; tmp1[K+1]=tmp2[K+1]=-1; int cnt=1,x=1,y=1; while(cnt!=K+1&&(tmp1[x]!=-1||tmp2[x]!=-1)) { if(tmp1[x]>tmp2[y])f[j][cnt]=tmp1[x],x++; else f[j][cnt]=tmp2[y],y++; if(f[j][cnt]!=f[j][cnt-1])cnt++; } } printf("%d ",f[V][K]); } return 0; }
区间DP
例1 [NOI1995]石子合并
首先断环成链,考虑设计状态,$f[l][r]$表示把第$l$到第$r$堆石子合并直到$l$,$r$相邻的分数,那么我们可以设计一个中间决策$k$,表示$l$~$r$可以由$l$~$k$和$k+1$~$r$转移过来,这就有点像$Floyd$算法的寻找中间点,有一点分治的意味在里面,但是我们合并$l$~$r$时,不仅仅有中间转移的分数贡献,而且还有$sum_{i=l}^{r}a_i$的分数贡献,因为每一次的合并算上的分数都是所有石子的分数贡献。
这里求了一个$min$和$max$,于是我们可以设计两个$f$数组,求出不同的分数。
注意边界问题和初始化,$max$和$min$要初始化为极大值或者极小值。
#include<bits/stdc++.h> using namespace std; int n,a[5005],b[5005],f[5005][5005]; int main() { ios::sync_with_stdio(false); cin>>n; for(int i=1;i<=n;i++)cin>>a[i]; for(int j=1;j<=n;j++)cin>>b[j]; for(int i=1;i<=n;i++) { int val=0; if(b[0]<a[i])val=f[i-1][0]; for(int j=1;j<=n;j++) { if(a[i]==b[j])f[i][j]=val+1; else f[i][j]=f[i-1][j]; if(b[j]<a[i])val=max(val,f[i-1][j]); } } int maxn=0; for(int i=1;i<=n;i++)maxn=max(maxn,f[n][i]); cout<<maxn<<" "; return 0; }
例2 NOIP2003 数字游戏
这道题首先仍然要断环成链,然后进行$O(n^4)$复杂度的$dp$,状态转移的过程仍然是在当前区间内寻找一个中间点(或者说断点),然后根据题目要求的$min/max$设计状态转移,这道题相比上一道题,看似更复杂,实际上不用考虑最后加一个前缀和,好理解很多,属于真正的区间$dp$入门。
#include<cstdio> #include<algorithm> #include<cstring> using namespace std; int n,m,maxn[105][105][15],minn[105][105][15],ma,mi,sum[105],a[105]; int mod(int a){return ((a%10)+10)%10;} int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&a[i]),a[i+n]=a[i]; for(int i=1;i<=2*n;i++) sum[i]=sum[i-1]+a[i]; memset(minn,0x7f7f7f7f,sizeof(minn)); for(int l=1;l<=2*n;l++) for(int r=1;r<=2*n;r++) maxn[l][r][1]=minn[l][r][1]=mod(sum[r]-sum[l-1]); for(int i=2;i<=m;i++) for(int l=1;l<=2*n;l++) for(int r=l+i-1;r<=2*n;r++) for(int k=l+i-2;k<r;k++) minn[l][r][i]=min(minn[l][r][i],minn[l][k][i-1]*mod(sum[r]-sum[k])),maxn[l][r][i]=max(maxn[l][r][i],maxn[l][k][i-1]*mod(sum[r]-sum[k])); mi=0x7f7f7f7f; for(int i=1;i<=n;i++) ma=max(ma,maxn[i][i+n-1][m]),mi=min(mi,minn[i][i+n-1][m]); printf("%d %d ",ma,mi); return 0; }
树形DP
全世界$DP$最菜的人居然在写树形$DP$的$Blog$,好耍了。
例1 没有上司的舞会
[传送门](https://www.luogu.org/problem/P1352)
这道题对应了树形$DP$的一个经典模型,就是关于$0/1$之间的树形$DP$,大概的模型为:
$f[u][1]+=max(f[v][1],f[v][0])$
第一维表示的是以节点$u$为根节点的子树,第二维表示选/不选。
#include<bits/stdc++.h> using namespace std; int n,f[12000][2],vis[6005],a[6005],h[12005],cnt; struct node{int v,nxt;}e[12005]; void add(int u,int v) { e[++cnt].v=v; e[cnt].nxt=h[u]; h[u]=cnt; } void dfs(int u,int fa) { f[u][1]=a[u]; for(int i=h[u];i;i=e[i].nxt) { int v=e[i].v; if(v==fa)continue; dfs(v,u); f[u][1]+=f[v][0]; f[u][0]+=max(f[v][0],f[v][1]); } } inline void read(int&x) { x=0;char c=getchar(); int f=1; while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();} while(c>='0'&&c<='9')x=x*10+c-'0',c=getchar(); x*=f; } int main() { read(n); for(int i=1;i<=n;i++)read(a[i]); for(int i=1;i<=n;i++) { int u,v; read(u),read(v); if(!u&&!v)break; vis[v]=true; add(u,v),add(v,u); } int rt; for(int i=1;i<=n;i++) if(!vis[i]){rt=i;break;} dfs(rt,0); printf("%d ",max(f[rt][0],f[rt][1])); return 0; }
例2 最大子树和
[传送门](https://www.luogu.org/problem/P1122)
很水的一道题,就是题意一点也不清晰。
#include<bits/stdc++.h> using namespace std; int n,a[200005],h[320005],cnt,f[320005],maxn,vis[320005]; struct node{int v,nxt;}e[320005]; void add(int u,int v) { e[++cnt].v=v; e[cnt].nxt=h[u]; h[u]=cnt; } void dfs(int u,int fa) { f[u]=a[u]; for(int i=h[u];i;i=e[i].nxt) { int v=e[i].v; if(v==fa)continue; dfs(v,u); f[u]+=max(f[v],0); } maxn=max(maxn,f[u]); } int main() { // freopen("testdata.in","r",stdin); scanf("%d",&n); for(int i=1;i<=n;i++)scanf("%d",&a[i]); for(int i=1;i<n;i++) { int u,v; scanf("%d%d",&u,&v); add(u,v),add(v,u); } dfs(1,0); printf("%d ",maxn); return 0; }
例3 选课
这道题是树上依赖背包的模板题。
我们可以这样思考:节点$u$的子树大小为$siz[u]$,那么在这棵树中我们最多能选出$siz[u]$个(当然要包含这个节点本身)节点作为答案的贡献,所以,这个节点对答案的贡献可以是$0$~$siz[u]$,那么我们可以设计状态$f[u][j]$用来表示在以$u$为根的子树中选$j$个节点的贡献, 针对每一个这个点的子节点,我们要么不选,要么选,选了这个点对应的情况就应该是,在节点$u$的子树中总共选择$j$个节点($j$对应的遍历就应该是$siz[u]$~$0$),同时我们要选择子节点$v$,所以我们就还要在$v$节点的子树中选择一些节点,这个节点数是否可以直接看成$j-1$呢?不可以,因为剩下的$j-1$个节点在$u$和$v$的子树中分布不能肯定,所以就要单独枚举一维$k$,从$j-1$遍历到$0$,这一步应该是已经处理出来的,不要问我为什么,于是最终得到伪代码:
$ exttt{for j siz[u]...0}$
$ exttt{for k j-1...0}$
$ exttt{f[u][j]=max(f[u][j],f[u][j-k-1]+f[v][k]+w[v])}$
于是我们发现,这就是一个分组背包模型(虽然似乎好像应该可能也许大概有点变化)。
还剩下一个问题就是,为什么两维循环都是倒序的呢?
观察第一维度,我们倒序的时候,求出的$j$答案就是由大到小转移而来,如果我们按照顺序排序,前面已经更新过的$f$就会去更新后面的$f$,这就要出问题。
第二维倒序,思想源自$01$背包,保证不重复。
#include<bits/stdc++.h> using namespace std; int n,m,w[305],f[305][305],siz[305],h[305],cnt; struct node{int v,nxt;}e[605]; void add(int u,int v) { e[++cnt].v=v; e[cnt].nxt=h[u]; h[u]=cnt; } void dfs(int u,int fa) { siz[u]=1; for(int i=h[u];i;i=e[i].nxt) { int v=e[i].v; if(v==fa)continue; dfs(v,u); siz[u]+=siz[v]; for(int j=siz[u];j>=0;j--) for(int k=j-1;k>=0;k--) f[u][j]=max(f[u][j],f[u][j-k-1]+f[v][k]+w[v]); } } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) { int fa; scanf("%d%d",&fa,&w[i]); add(fa,i),add(i,fa); } dfs(0,-1); printf("%d ",f[0][m]); return 0; }
状压DP
仔细想想,我讲不来状压DP,因为我也是个初学的萌新。
状压的核心就是,利用二进制位来枚举状态。
直接上题吧。
[SCOI2005]互不侵犯
题目描述:
在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个格子。(1<=N <=9, 0 <= K <= N * N)
分析:
这是一道状压的经典入门题。由于是矩阵,在状压的套路中要么枚举行要么枚举列,这里我们就枚举一波行数。
我们考虑,每一行有(1<<n-1)种状态,有合法也有不合法,我们不妨预处理一行中能出现的所有合法状态,那么针对行的合法状态是什么呢?就是不能相邻两个都为1,“都为1”我们显而易见能想到&运算。
如果有一个状态是i,如果i&(i<<1)的结果为1,那么就说明i这个状态是不合法的。为什么呢?举个例子:
有一个状态i为0110,人眼判断是不合法的,这个状态左移就应该是1100,两个状态相&,答案为真,所以状态i 0110就不合法。
所以我们就得到了预处理i是否合法的方法:i&(i<<1)。
下一步需要预处理出所有的状态数和每一个状态有多少个1(也就是说有多少个国王),这里也是位运算的知识qwq,于是我们可以得到:
for(int i=0;i<=(1<<n)-1;i++)//i表状态 { if(i&(i<<1))continue; s[++cnt]=i; num[cnt]=get(i); f[1][cnt][num[cnt]]=1; }
这当中有一个f数组是干什么的呢?我们设f[i][j][k]表示:到达第i行的状态j时,总共放了k个国王的方案数。
由于第一行不可以通过状态转移得到,所以我们预处理出来,如果这个状态合法,f[1][j][num[j]]的方案数就应该是1。
状态该怎么转移呢?第i行的某个状态应该由第i-1行的某个状态转移而来:
f[i][j][l]=f[i-1][k][l-num[j]]
只需要判断j和k这两个状态同时存在是否合法即可,合法才能进行转移,否则跳过。
不合法的判断:
if((s[j]&s[k])||((s[j]<<1)&s[k])||((s[k]<<1)&s[j]))continue;
Points:
这道题是求一个总方案数,一个新的状态可以由多种不同方案转移而来,所以新的状态要加上所有能转移出它的所有状态的方案数,这里的j和k都表示的是第几种状态,所以范围是1~cnt,而l至少应该等于num[j],才能保证放的国王是足量的,也就是说在i,j状态时,总共放了l个国王,那么这肯定是l-num[j]的每一个状态转移而来的。
感觉自己说了好多废话..无所谓,能理解就行。
统计方案数就应该是最后一排每一个状态选了k个国王的总和,毕竟是统计总方案数。
代码:
#include<cstdio> #include<cstring> #include<algorithm> #define int long long using namespace std; int n,m,f[20][1<<10][105],s[1<<10],num[1<<10],cnt;//f[i][j][k] 第i行状态为第j种 这一行有k个国王 int get(int pos) { int ans=0; while(pos) { if(pos&1) ans++; pos>>=1; } return ans; } signed main() { scanf("%lld%lld",&n,&m); for(int i=0;i<=(1<<n)-1;i++)//i表状态 { if(i&(i<<1))continue; s[++cnt]=i; num[cnt]=get(i); f[1][cnt][num[cnt]]=1; } for(int i=2;i<=n;i++) for(int j=1;j<=cnt;j++) for(int k=1;k<=cnt;k++) { if((s[j]&s[k])||((s[j]<<1)&s[k])||((s[k]<<1)&s[j]))continue; for(int l=num[j];l<=m;l++) f[i][j][l]+=f[i-1][k][l-num[j]]; } int ans=0; for(int i=1;i<=cnt;i++) ans+=f[n][i][m]; printf("%lld ",ans); return 0; }
Matrix
题面不公开,在此做个小总结。
1.DP的核心思想是枚举,把我们要求但不能直接得到的决策进行枚举,适用于任何DP。
2.矩阵状压有个特色就是,存行或者列,枚举另一维的所有状态。
3.状压的核心思想是位运算,除此之外和普通DP差别不大,所以我们要熟练掌握各类位运算。
4.状压的过程当中常常会遇到很多的要求,要求不相邻等等,我们要学会把这些要求转化成位运算的代码。
5.初始化,状态与状态转移,这就是DP的过程 ,但我们只有找到了转移的方法,才知道每一个状态是怎么来的,自然也就知道了初始化。
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; int n,m,chu[15],f[15][1050][1050],g[15][15],w[15][1050],fg[1050]; int main() { freopen("matrix.in","r",stdin); freopen("matrix.out","w",stdout); memset(f,0x7f7f7f7f,sizeof(f)); scanf("%d%d",&n,&m); int all=(1<<m)-1; for(int i=1;i<=n;i++) { char s[15]; scanf("%s",s+1); for(int j=1;j<=m;j++) chu[i]=(chu[i]<<1)|(s[j]-'0');//初始化每一行的状态 } for(int i=1;i<=n;i++) { for(int j=1;j<=m;j++) scanf("%d",&g[i][j]); for(int j=0;j<=all;j++) for(int k=1;k<=m;k++) if((j>>(m-k))&1)w[i][j]+=g[i][k]; //针对选择区间进行初始化 第i行的第j个状态进行初始化 } for(int i=0;i<=all;i++)fg[i]=(i|(i<<1)|(i>>1))&all;//初始化选择区间的覆盖区间 for(int i=0;i<=all;i++)f[1][fg[i]|chu[1]][i]=w[1][i];//i枚举的是选择区间 for(int i=1;i<=n;i++)//枚举第几行 for(int j=0;j<=all;j++)//枚举第i行的状态j for(int k=0;k<=all;k++)//枚举第j个状态的选择区间k { if((j&fg[k])!=fg[k])continue; for(int l=0;l<=all;l++)//枚举第i+1行的选择区间 { if((l|j)!=all)continue; f[i+1][chu[i+1]|k|fg[l]][l]=min(f[i+1][chu[i+1]|k|fg[l]][l],f[i][j][k]+w[i+1][l]); } } int ans=0x7f7f7f7f; for(int i=0;i<=all;i++) ans=min(ans,f[n][all][i]); printf("%d ",ans); return 0; }