0x56 内容简介与例题习题

《算法竞赛进阶指南》读书笔记汇总
这里面是我在阅读《算法竞赛进阶指南》这本书时的一些思考,有兴趣可以瞧瞧!
如若发现什么问题,可以通过评论或者私信作者提出。希望各位大佬不吝赐教!
许多问题都可以使用二叉堆来进行优化,下面直接看例题吧。

状压dp基本介绍

对于某些问题,我们需要在动态规划的“状态”中记录一个集合,保存“轮廓”的详细信息,以便进行状态转移。

通常,若所记录集合中的元素都是小于 K K K的自然数,且集合大小不超过 N N N,那么我们可以将这个集合表示成一个 N N N K K K进制数,用一个 [ 0 , K N − 1 ] [0,K^{N-1}] [0,KN1]范围内的十进制整数,作为状态中的某一维。这种把集合表示成整数记录dp状态的方法我们称为状态压缩动态规划。然后由于整数范围有限,这类问题的一般特征就是 N N N很小。
让我们通过几道题熟悉一下状压dp的基本应用。

【例题】蒙德里安的梦想(AcWing291)

题目链接
思路:这是一道状态压缩dp的经典例题。首先我们先观察一下题目的特点,我们按行来看,对于某一行 i i i的空格而言,只有三种情况,要么是 2 ∗ 1 2*1 21小方格的上半部分,要么是 2 ∗ 1 2*1 21小方格的下半部分,要么是 1 ∗ 2 1*2 12小方格一部分。如图:
在这里插入图片描述
对于第一种而言,下一行必须补充完这个长方形的下半部分,对于第二种和第三种而言,下一行放哪种都可以(当然也要合法,接下来会说到)
那么有了这个特性之后,我们可以用 1 1 1表示第一种情况,用 0 0 0表示第二种和第三种情况,比如下图:
在这里插入图片描述
那么某一行的状态就可以表示成一个二进制数啦!
我们定义 f [ i ] [ j ] f[i][j] f[i][j]表示第 i i i行的状态为 j j j时,前 i i i行分割方案的总数,其中 j j j是用十进制整数记录的 M M M位二进制数。那么初始状态为 f [ 0 ] [ 0 ] f[0][0] f[0][0],目标状态为 f [ N ] [ 0 ] f[N][0] f[N][0]
下面我们考虑状态转移:
由于影响到第 i i i行状态的只有第 i − 1 i-1 i1行,那么我们考虑第 i i i行的状态 j j j是否可以由第 j j j行的状态 k k k转移过来。不难发现,当 k k k中某一位为 1 1 1时, j j j中对应的那一位必须为 0 0 0;当 k k k中某一位为 0 0 0时, j j j中对应的那一位可以为 1 或 0 1或0 10。所以第一个条件为:
(1)j & k == 0
另外,我们还需要判断k中放第三种情况的连续位置个数是否为偶数个,就是横着放是否可以放得下。那么如何得到那些横着放的位置呢。我们发现, j ∣ k j | k jk之后得到的二进制数中, 0 0 0的位置就是横着放的位置,判断一下连续 0 0 0的个数是否为偶数即可,可以预处理所有状态是否合法(用 s t st st数组)。所以第二个条件就是:
(2)st[j | k] == true
那么就可以轻松的写出代码啦!

AC代码:

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

typedef long long LL; 

const int N = 12;

LL f[N][1 << N];
bool st[1 << N];
int n, m;

int main()
{
	while(cin >> n >> m, n || m)
	{
		for (int i = 0; i < 1 << m; i ++ )
		{
			bool flag = 0, cnt = 0;
			for (int j = 0; j < m; j ++ )
			 	if (i >> j & 1) flag |= cnt, cnt = 0;
				else cnt ^= 1;
			st[i] = flag | cnt ? 0 : 1;		
		}		
		
		memset(f, 0, sizeof f);
		f[0][0] = 1;
		
		for (int i = 1; i <= n; i ++ )
			for (int j = 0; j < 1 << m; j ++ )
				for (int k = 0; k < 1 << m; k ++ )
					if ((j & k) == 0 && st[j | k])
						f[i][j] += f[i - 1][k];
		
		cout << f[n][0] << endl;
	}
	return 0;	
}

【例题】炮兵阵地(AcWing292)

题目链接
思路:这一题和上一题差不多,但是这一题影响第 i i i行状态的有前两行,所以我们的状态需要加上一维。我们定义 f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k]表示,前 i i i行已经安排完,第 i i i行的状态为 k k k,第 i − 1 i - 1 i1行的状态为 j j j,并且所有炮兵满足题目要求,炮兵数量的最大值。
转移的时候,我们枚举第 i − 2 i - 2 i2行的状态,判断状态是否合法即可。判断合法也比较简单,满足以下条件即可:
(1)对于某个行状态,某两个相邻炮兵距离不能小于等于2
(2)对于某两个相邻行状态,炮兵所处位置不能在同一列
(3)炮兵不能放在山地上
详细实现见代码。
这题还卡空间,用滚动数组优化即可。

AC代码:

#include <iostream>

using namespace std;

const int N = 110, M = 11;

int n, m;
int f[3][1 << M][1 << M];
int g[N];
char G[N][M];
int ones[1 << M];


int main()
{
	scanf("%d%d", &n, &m);
	
	for (int i = 0; i < 1 << m; i ++ )
		for (int j = 0; j < m; j ++ )
			if (i & (1 << j))
				ones[i] ++;
				
	for (int i = 1; i <= n; i ++ )
	{
		scanf("%s", G[i] + 1);
		for (int j = 1; j <= m; j ++ )
			if (G[i][j] == 'H') g[i] += (1 << (j - 1));
	}
	
	for (int i = 0; i < 1 << m; i ++ ) 
		if (!(i & g[1] || (i & (i << 1)) || (i & (i << 2))))
			f[1][i][0] = ones[i];
			
	for (int i = 0; i < 1 << m; i ++ )
		for (int j = 0; j < 1 << m; j ++ )
			if (!(i & j || i & g[2] || j & g[1] || (i & (i << 1)) || (i & (i << 2)) || (j & (j << 1)) || (j & j << 2)))
				f[2][i][j] = ones[i] + ones[j];
	
	for (int i = 3; i <= n; i ++ )
	{
		for (int j = 0; j < 1 << m; j ++ )
		{
			if (j & g[i] || (j & (j << 1)) || (j & (j << 2)))
				continue;
			for (int k = 0; k < 1 << m; k ++ )
			{
				if (k & j || k & g[i - 1] || (k & (k << 1)) || (k & (k << 2)))
					continue;
				for (int hh = 0; hh < 1 << m; hh ++ )
				{
					if (hh & k || hh & j || hh & g[i - 2] || (hh & (hh << 1)) || (hh & (hh << 2)))
						continue;
					f[i % 3][j][k] = max(f[i % 3][j][k], f[(i - 1) % 3][k][hh] + ones[j]);	
					
				}	
			}	
		}
	}

	int ans = 0;
	for (int i = 0; i < 1 << m; i ++ )
		for (int j = 0; j < 1 << m; j ++ )		
			ans = max(f[n % 3][i][j], ans);
			
	printf("%d\n", ans);
	return 0;
}

另类思路:这题还有另一个做法,就是用三进制的状态压缩。我们用 2 2 2表示这个格子放了炮兵,用 1 1 1表示这个格子不放炮兵,同一列上一行的格子放了炮兵,用 0 0 0表示这个格子不放炮兵,同一列上一行的格子也不放炮兵。那么根据题意,只有 0 0 0的下一行才能放 2 2 2
我们考虑前一行的每一个状态可以转移向当前行的哪些状态,需要满足以下条件:
(1)当第 i i i行第 j j j列为 2 2 2时,第 i + 1 i + 1 i+1行第 j j j列必须为1
(2)当第 i i i行第 j j j列为 1 1 1时,第 i + 1 i + 1 i+1行第 j j j列必须为0
(3)山地格子不能填 2 2 2
(4)如果一个格子填了 2 2 2,他后面的两个格子就不能填 2 2 2
我们定义 f [ i ] [ j ] f[i][j] f[i][j]表示前 i i i行已经安排完,第 i i i行状态为 j j j的最大炮兵数目。其中 j j j是一个用十进制整数表示的三进制数。转移的时候直接dfs出下一行的所有合法状态即可。
细节看代码吧

AC代码:

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 105, M = 60000;

int n, m;
char str[N][11];
int f[N][M];
bool g[N][11];
int pre[11], cur[11];

void Ten_to_Three(int x, int a[])
{
    for (int i = m - 1; i >= 0; i -- )
    {
        a[i] = x % 3;
        x /= 3;
    }
}

void Three_to_Ten(int& x, int a[])
{
    x = 0;
    for (int i = 0; i < m; i ++ )
        x = x * 3 + a[i];
}

void dfs(int row, int col, int sum)
{
    if (col == m)
    {
        int res;
        Three_to_Ten(res, cur);
        f[row][res] = max(f[row][res], sum);
        return;
    }
    
    dfs(row, col + 1, sum);
    if (!g[row][col] && !pre[col] && (col < 1 || cur[col - 1] < 2) && (col < 2  || cur[col - 2] < 2))
    {
        cur[col] = 2;
        dfs(row, col + 1, sum + 1);
        cur[col] = 0;
    }
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ )
    {
        scanf("%s", str[i]);
        for (int j = 0; j < m; j ++ )
            g[i][j] = (str[i][j] == 'H');
    }
    
    memset(f, -1, sizeof f);
    f[0][0] = 0;
    
    int up = 1;
    for (int i = 1; i <= m; i ++ )
        up *= 3;
    
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < up; j ++ )
            if (f[i][j] != -1)
            {
                Ten_to_Three(j, pre);
                for (int k = 0; k < m; k ++ )
                    cur[k] = max(pre[k] - 1, 0);
                dfs(i + 1, 0, f[i][j]);
            }
        
    int ans = 0;
    for (int i = 0; i < up; i ++ )
        ans = max(ans, f[n][i]);
    printf("%d\n", ans);
    
    return 0;
}

习题

【练习】岛和桥(AcWing1194)

题目链接
思路:由题意得,每一个点都只会经过一次,我们不难想到用二进制数表示每一个点是否被经过,然后由于题目要求的权值涉及一个三元组,那么与上一题一样,影响当前点也就是最后一个经过的点的点有两个,就是倒数第二个和倒数第三个经过的点。那么我们可以定义 f [ s t a t e ] [ i ] [ j ] f[state][i][j] f[state][i][j]表示当前已经过点的集合为 s t a t e state state,最后一个经过的点为 j j j,倒数第二个经过的点为 i i i,满足条件所有方案中权值的最大值。
转移的时候可以枚举倒数第三个经过的点 k k k进行转移。
接下来就是合法性的判定啦。也比较简单,三个点都必须两两不同且 i i i j j j必须相连, k k k i i i必须相连。
路径数目,就在求最大值的时候顺便用数组存一下就可以了。

AC代码:

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 15;

int val[N];
int r[N][N];
int f[1 << N][N][N], g[1 << N][N][N];
int n, m;

int main()
{
    int T;scanf("%d", &T);
    while (T --)
    {
        memset(r, 0, sizeof r);
        memset(f, -0x3f, sizeof f);
        
        scanf("%d%d", &n, &m);
        int sum = 0;
        for (int i = 0; i < n; i ++ )
        {
            scanf("%d", &val[i]);
            sum += val[i];
        }
        for (int i = 0; i < m; i ++ )
        {
            int u, v;
            scanf("%d%d", &u, &v);
            u --, v --;
            r[u][v] = r[v][u] = val[u] * val[v];
        }
        
        for (int i = 0; i < n; i ++ )
            f[1 << i][i][n] = 0, g[1 << i][i][n] = 1;
            
        for (int state = 0; state < 1 << n; state ++ )
            for (int i = 0; i < n; i ++ )
            {
                if (!(state >> i & 1)) continue;
                int pre = state ^ (1 << i);
                for (int j = 0; j < n; j ++ )
                {
                    if (!(pre >> j & 1)) continue;
                    if (!r[i][j]) continue;
                    int tmp = pre ^ (1 << j) ^ (1 << n);
                    for (int k = 0; k <= n; k ++ )
                    {
                        if (!(tmp >> k & 1)) continue;
                        int t = f[pre][j][k] + r[i][j] + (r[i][k] ? r[i][k] * val[j] : 0);
                        if (t > f[state][i][j])
                        {
                            f[state][i][j] = t;
                            g[state][i][j] = g[pre][j][k];
                        }
                        else if (t == f[state][i][j])
                            g[state][i][j] += g[pre][j][k];
                    }
                }
            }
            
        int ans = 0, num = 0;
        for (int i = 0; i < n; i ++ )
            for (int j = 0; j < n; j ++ )
                if (ans < f[(1 << n) - 1][i][j])
                {
                    ans = f[(1 << n) - 1][i][j];
                    num = g[(1 << n) - 1][i][j];
                }
                else if (ans == f[(1 << n) - 1][i][j])
                {
                    num += g[(1 << n) - 1][i][j];
                }
                
        if (ans) printf("%d %d\n", ans + sum, num / 2);
        else puts("0 0");
    }
    return 0;
}

芯片(AcWing328)

题目链接
暂未完成

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值