动态规划模型总结之状压dp

啊怎么前面的都没写就直接状压了啊

状压比较简单先讲状压(其实比较像深搜的优化)

可能蓝书上的例题偏难先将一些简单的

不会位运算就gg了吧

1.例题引入

首先,有很多问题由于空间限制或其他原因,我们不能通过原来的方式直接存储,而聪明的人类就把他们压缩到一个很小的单位去计算,用这种方式去操作的就有大名鼎鼎的哈希算法,还有状压算法

为什么需要状压dp?

状压dp可以解决一些题目的状态随阶段增长而增长的题目,比如一个量用了还是没用等等,把状态压缩成数字存入数组然后进行dp

啊听着好抽象啊

那我们搞一个例题看看

状压dp入门题:玉米田

简化后的题目意思是有一块矩阵,可以取一些1,但是不能有相邻的1,问有多少种取法

此题是否可以 d f s dfs dfs?应该是可以的,但是会TLE

我们发现在 d f s dfs dfs的是后需要记录当前位置取没取,这样才能判断该位置可不可以去

这提示我们可以使用状压 d p dp dp

我们首先预处理出每一行有哪些取的方案合法,即不能取0也不能有相邻

不能取0比较简单,也就是把每行的草地情况压成int(存到 f f f数组),然后如果枚举到一个状态 s t a sta sta,如何判断这个 s t a sta sta没有零?就是他与该行的 f f f的且是否还是原数,因为如果不是原数,说明他选取了一些0,否则肯定是合法的

那如何判断一个状态没有相邻的?这个就要考察位运算的功底了,其实就是左移一位和右移一位都与原数没有交

预处理出这些就可以进入最后的 d p dp dp环节

d p [ i ] [ s t a ] dp[i][sta] dp[i][sta]表示第 i i i行现在的状态为 s t a sta sta

然后枚举之前的状态为 m a s k mask mask,如果 s t a sta sta m a s k mask mask没有交,则把 d p [ i − 1 ] [ m a s k ] dp[i-1][mask] dp[i1][mask]的值加到 d p [ i ] [ s t a ] dp[i][sta] dp[i][sta]里面去

答案就是 ∑ d p [ n ] [ s t a ] \sum{}dp[n][sta] dp[n][sta]

我们发现如果第 i i i行的取法只与 i − 1 i-1 i1行和他自己有关,于是可以把 i i i这一维滚动掉

时间复杂度 O ( n ∗ 2 n ∗ 2 n ) O(n*2^n*2^n) O(n2n2n)

n n n才12,正好

const int p = 100000000 ;
int dp[13][5000],a[13][13],f[13],ok[5000];
int n,m ;
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++)
    f[i]=(f[i]<<1)+a[i][j] ;
    for (int i=0;i<(1<<m);i++) ok[i]=((!(i&(i<<1)))&&(!(i&(i>>1)))) ;
    dp[0][0]=1 ;
    for (int i=1;i<=n;i++){
        for (int j=0;j<(1<<m);j++) {
            if (ok[j] && ((j&f[i])==j)){
                for (int k=0;k<(1<<m);k++)
                if (!(j&k)){
                    dp[i][j]=(dp[i][j]+dp[i-1][k])%p ;
                }
            }
        }
    }
    int ans=0;
    for (int i=0;i<(1<<m);i++) ans=(ans+dp[n][i])%p ;
    printf("%d\n",ans) ;
} 

如果您觉得这个题目还比较困难,不妨先做一个小练习:

Water

题目描述

n n n个装着水的开水瓶,要把它们中的水汇总到不超过 k k k个开水瓶里,把第 i i i个开水瓶里的水全都倒到第 j j j个开水瓶里(无论它们现在装了多少水)的代价是 c i j c_{ij} cij。求最小总代价。

输入格式

第一行两个整数 n , k n,k n,k

接下来的 n n n 行,每行 n n n 个整数表示 c i j c_{ij} cij ,保证 c i i = 0 c_{ii}=0 cii=0

输出格式

输出一行一个整数表示答案。

样例

input

5 2
0 5 4 3 2
7 0 4 4 4
3 3 0 1 2
4 3 1 0 5
4 5 5 5 0 

output

5

数据范围

1 ≤ k ≤ n ≤ 20 , c i j ≤ 1 0 5 1≤k≤n≤20,c_{ij}≤10^5 1kn20,cij105

如果您上一个题目听懂了,这个就是小case了

d p [ m a s k ] dp[mask] dp[mask]表示瓶子状态为 m a s k mask mask最小需要花费的代价

然后枚举把 i i i号瓶子的水转移到 j j j号瓶子就好了

然后如果中间1的个数 &lt; = k &lt;=k <=k的话就更新答案

时间复杂度 O ( n 2 ∗ 2 n ) O(n^2*2^n) O(n22n)

int n, k, ans = iinf ;
int dp[N], a[25][25] ;

int calc(int x) {
	int res = 0 ;
	while (x) {
		x = x & x - 1 ;
		res++ ;
	}
	return res ;
}

signed main(){
	scanf("%d%d", &n, &k) ;
	if (n == k) print(0) ;
	for (int i = 0; i < n; i++)
	for (int j = 0; j < n; j++)
	scanf("%d", &a[i][j]) ;
	for (int i = 0; i <= (1 << n) - 1; i++) dp[i] = iinf ;
	dp[(1 << n) - 1] = 0 ;
	for (int s = (1 << n) - 1; s >= 0; s--) { // 枚举当前状态
		if (calc(s) <= k) ans = min(ans, dp[s]) ;
		for (int i = 0; i < n; i++) // 把第i个水壶中的水
		if (s & (1 << i))
		for (int j = 0; j < n; j++) // 转移到第j个水壶中
		if (i != j) {
			dp[(s - (1 << i)) | (1 << j)] = min(dp[(s - (1 << i)) | (1 << j)], dp[s] + a[i][j]) ;
		}
	}
	printf("%d\n", ans) ;
}

2.算法特点

能够通过状压dp解决的题目的数据基本都有一个共同点:就是数据范围很小,都是10~20差不多的

基本上都是要求记录没一个变量的取值情况什么的

还有什么呢,也就是可以 d p dp dp 呗φ(>ω<*)

3.题目选讲

先把蓝书上的题目刷了在看下面呗

宝藏

noip题,是状压dp第一次也是唯一一次出现在noip赛场上

数据范围提示性很强

我们对于每个点做一次最短路,求出 d i s dis dis表示每个点的距离

然后再改一个 d p [ i ] dp[i] dp[i]数组表示当前选择情况为 i i i的最优方案代价

然后通过 d f s dfs dfs去记忆化搜索搜出答案

答案就是 min ⁡ s = 1 n d p [ 2 n − 1 ] \min_{s=1}^ndp[2^n-1] mins=1ndp[2n1]

const int N = 15;
const int M = (1<<15) ;

int g[N][N] ;
int dp[M] ;
int dis[N] ;
int n,m ;

void dfs(int x){
    for (int i=1;i<=n;i++){
        if (x&(1<<(i-1))){
            for (int j=1;j<=n;j++){
                if ((x&(1<<(j-1)))==0 && g[i][j]!=inf){
                    if (dp[x]+dis[i]*g[i][j]<dp[x|(1<<(j-1))]){
                        int s=dis[j] ;
                        dis[j]=dis[i]+1;
                        dp[x|(1<<(j-1))]=dp[x]+dis[i]*g[i][j] ;
                        dfs(x|(1<<(j-1))) ;
                        dis[j]=s ;//重置 
                    }
                }
            }
        }
    }
}

int main(){
    scanf("%d%d",&n,&m);
    memset(g,0x3f,sizeof(g)) ;
    for (int i=1;i<=m;i++){
        int x,y,z ;
        scanf("%d%d%d",&x,&y,&z) ;
        g[x][y]=min(g[x][y],z) ; 
        g[y][x]=min(g[y][x],z) ;
    }
    int ans=inf ;
    for (int i=1;i<=n;i++){
        memset(dp,0x3f,sizeof(dp)) ;
        memset(dis,0x3f,sizeof(dis));
        dis[i]=1;
        dp[1<<(i-1)]=0;
        dfs(1<<(i-1)) ;
        ans=min(ans,dp[(1<<n)-1]) ;
    }
    printf("%d\n",ans) ; 
    return 0 ;
}

Contest

出在noip也挺适合的

题意
  
n n n个问题,解决的顺序影响正确的概率,无论之前解决的问题是否答对,当前问题 j j j 答对概率为 m a x ( a [ i ] [ j ] ) max(a[i][j]) max(a[i][j]) ( i i i为解决过的问题)。求答对题目的最大期望和对应的答题顺序。

分析

假设当前状态是 i i i i i i 对应的01串的1代表已经解决的问题,0代表尚未解决的,那么它肯定是由某个已解决的问题还没解决的状态转移过来的,也就是由其中一个1变为0的状态。所以我们枚举它里面的每个1, i   &amp;   ( 2 l ) = 1 i\ \&amp;\ (2^l) =1 i & (2l)=1 表示第l个数字是1, j = i − ( 2 l ) j = i-(2^l) j=i(2l) 得到 j j j 状态。

d p dp dp表示状态i的期望, d p [ i ] = d p [ j ] + m a x ( a [ i ] [ j ] ) dp[i]=dp[j] + max(a[i][j]) dp[i]=dp[j]+max(a[i][j]) 。因为多答出一题,期望就增加(1*答出这题的概率)。

同时每个状态用 d d d储存答题顺序(期望最大且字典序最小的答题顺序)。

这句判断字典序: m x = d p [ i ]   &amp; &amp;   d [ i ] &gt; = d [ i − ( 2 j ) ] mx=dp[i] \ \&amp;\&amp; \ d[i]&gt;=d[i-(2^j)] mx=dp[i] && d[i]>=d[i(2j)]。如果新的 d p [ i − ( 2 j ) ] dp[i-(2^j)] dp[i(2j)]的字典序比较小, d p [ i ] dp[i] dp[i]更新。因为一开始 d d d都是0,所以要用≥,不然第一个样例就不会输出A。

int T, n ;
int a[N][N] ;
string ans[N] ;
int dp[N] ;

signed main(){
	scanf("%d", &T) ;
	while (T--) {
		scanf("%d", &n) ;
		for (int i = 0; i < n; i++)
		for (int j = 0; j < n; j++)
		scanf("%d", &a[i][j]) ;
		ans[0] = "" ; clr(dp) ;
		for (int i = 0; i < (1 << n); i++)
		for (int j = 0; j < n; j++)
		if (i & (1 << j)){
			int mx = 0 ;
			for (int k = 0; k < n; k++) if (i & (1 << k) && a[k][j] > mx) mx = a[k][j] ;
			mx += dp[i - (1 << j)] ;
			char x = 'A' + j ;
			if (mx > dp[i] || (mx == dp[i] && ans[i] >= ans[i - (1 << j)])) {
				dp[i] = mx ;
				ans[i] = ans[i - (1 << j)] + x ;
			}
		}
		double res = dp[(1 << n) - 1] * 1.0 / 100 ;
		printf("%.2lf\n", res) ;
		cout << ans[(1 << n) - 1] << endl ;
	}
	return 0 ;
}

状态压缩dp不单单能够独自出现,还能和其他算法结合,一般具有很高的难度。

例如Password这题

这个题目真的很难

看到区间取反操作,除了线段树之外,还能够想到一种加速的方式:差分。我们将无需点亮的看做0,需要点亮的看做1,那么原序列就变成了一个01序列。我们在最后补上一个0,然后对其进行差分,这样原序列的一段1就对应了差分数组上的两个1(如果最后不补0那么原串结尾的1在差分数组上就只会有一个1,对于之后的操作就比较麻烦)

考虑每一次区间取反操作的实质,长度为K的区间取反操作在差分数组上体现为两个间隔为 K K K的数取反。显然将两个0变成1不会是最优情况,那么我们可以认为:在差分数组上对于一个0和一个1的一次区间取反操作就是1向旁边走了 K K K格,而两个1的取反操作就是两个1走到了一起然后被消除。

发现需要点亮的点最多只有10个,那么差分数组中的1最多只有20个,那么我们可以通过最短路计算差分数组上第 i i i个1和第 j j j个1走到一起的最小操作次数 f i , j f_{i,j} fi,j,然后通过状压 D P DP DP计算消除 i i i集合中的1的操作次数 g i g_i gi,大力转移即可。

int n, k, l ;
int x[N], a[N], dis[N], cost[22][22], id[N], vis[N], dp[1 << 22] ;
vector <int> g[N] ;

void add(int x, int y){
    g[x].pb(y) ; g[y].pb(x) ;
}


void spfa(int s) {
    clr(vis) ;
    queue <int> q ;
    for (int i = 0; i < N; i++) dis[i] = iinf ;
    dis[s] = 0 ; vis[s] = 1 ; q.push(s) ;
    while (!q.empty()) {
        int now = q.front() ; q.pop() ;
        vis[now] = 0 ;
        for (int i = 0; i < SZ(g[now]); i++) {
            int to = g[now][i] ;
            if (dis[to] > dis[now] + 1) {
                dis[to] = dis[now] + 1 ;
                if (!vis[to]) vis[to] = 1, q.push(to) ;
            }
        }
    }
}

void douout(double x){
     printf("%lf\n", x + 0.0000000001) ;
}

signed main(){
    scanf("%d%d%d", &n, &k, &l) ;
    clr(x) ;
    for (int i = 1; i <= k; i++) {
        int tmp ;
        scanf("%d", &tmp) ;
        x[tmp] = 1 ;
    }
    for (int i = 0; i <= n; i++)
    if (x[i] != x[i + 1]) x[i] = 1 ;
    else x[i] = 0 ;
    for (int i = 1; i <= l; i++) scanf("%d", &a[i]) ;
    for (int i = 0; i <= n; i++)
    for (int j = 1; j <= l; j++) {
        if (i - a[j] >= 0 && x[i - a[j]] == 0) add(i - a[j], i) ;
        if (i + a[j] <= n) add(i, i + a[j]) ;
    }
    k = 0 ;
    for (int i = 0; i <= n; i++)
    if (x[i]) id[i] = k++ ;
    memset(cost, 0x3f, sizeof(cost)) ;
    for (int i = 0; i <= n; i++)
    if (x[i]) {
        spfa(i) ;
        for (int j = 0; j <= n; j++) if (x[j]) cost[id[i]][id[j]] = dis[j] ;
    }
    memset(dp, 0x3f, sizeof(dp)) ;
    dp[0] = 0 ;
    int K = 1 << k ;
    for (int i = 1; i < K; i++) {
        int x, y ;
        for (x = 0; x < k; x++)
        if ((i >> x) & 1) break ;
        for (y = 0; y <= k; y++)
        if ((i >> y) & 1)
        dp[i] = min(dp[i], dp[i ^ (1 << x) ^ (1 << y)] + cost[x][y]) ;
    }
    if (dp[K - 1] == 0x3f3f3f3f) printf("-1") ;
    else printf("%d\n", dp[K - 1]) ;
}

4.作业

Hie with the Pie (状压dp入门题)

互不侵犯(中档,可以打表)

NOI2015寿司晚宴(相比CF79D简单不少,但还是值得做的)

Thanks for your reading♪(・ω・)ノ

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值