《信息学奥赛一本通·提高篇》动态规划第4节—状态压缩类动态规划

《信息学奥赛一本通·提高篇》动态规划第4节—状态压缩类动态规划

【例 1】国王

在这里插入图片描述

#include <iostream>
#include <vector>

using namespace std;

typedef long long LL;//发现答案会溢出,所以开LL

const int N = 12;//最后输出答案的时候再解释
const int M = 1 << 10, K = 110;//所有状态的数量,国王的数量

int n, m;
vector<int> state;//所有合法状态
vector<int> head[M];//当前状态能转移到的所有合法状态
int cnt[M];//当前状态1(国王)的个数
LL f[N][K][M];//f[i][j][k]表示 前i层放置了j个国王,且第i层状态是k的方案

inline bool check(int st) {return !(st & (st >> 1));}

inline int count(int st)
{
    int res = 0;
    for (int i = 0; i < n; i ++) res += st >> i & 1;
    return res;
}

int main()
{
	cin >> n >> m;
	//对所有状态预处理,把合法状态记录下来
	for (int i = 0; i < (1<<n); i ++)
		if (check(i))
		{
			state.push_back(i);
			cnt[i] = count(i);
		}
		
	//枚举行的所有状态,看看哪些状态和哪些状态之间可以转移
    for (int a : state)
        for (int b : state)
			//相同的位置不能同时有国王,并且上下两行的国王位置不能紧挨着
			/*
			    0 1 0 0 国王的位置在2
			    0 0 0 1 国王的位置在4 这种状态就是合法的
			    0 1 0 0  0 1 0 0
			    0 1 0 0  0 0 1 0  这样的状态都是不合法的
			*/
			if(!(a & b) && check(a | b)) 
				head[a].push_back(b);
		
	f[0][0][0] = 1;//初始化
	//因为最后要输出所有的方案数,采用一个小技巧,就是我们在枚举i的时候,枚举到n+1就可以了
	//假设我们的棋盘是一个n+1 * n的一个棋盘,多了一行,但是最后一行什么都不放
	//所以这里的n最大是11,下标是11的话,数组当然就要开12了,对应上面的N = 12
	for (int i = 1; i <= n + 1; i ++)
		for (int j = 0; j <= m; j ++)//国王数量 这里必须从0开始,因为有的行可以不放国王
			for (int a : state) if (j >= cnt[a])//枚举一下第i行所有的状态,同时国王数量必须足够
				for (int b : head[a])//枚举所有能转移到a的状态(上一行状态)
					f[i][j][a] += f[i - 1][j - cnt[a]][b];

    //输出第n + 1行,m个国王,第n + 1行一个国王都没有的情况数量			
	cout << f[n + 1][m][0];
	
	return 0;
}

【例 2】牧场的安排

在这里插入图片描述

#include <iostream>
#include <cstdio>
#include <vector>

using namespace std;

const int N = 14, M = 1 << 12, mod = 1e8;

int n, m;
int g[N];//记录每一行方格的状态,用二进制数表示
vector<int> state;//所有合法状态
vector<int> head[M];//当前状态能转移到的所有合法状态
int f[N][M];//f[i][j]表示前i层,且第i层状态为j的方案数量	

//检查当前状态是否合法 任意一个种玉米的格子左边不能种玉米
bool check(int st) {return !(st & (st >> 1));}

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++)
        for (int j = 0; j < m; j ++)
        {
            int t; cin >> t;
            g[i] += (!t << j);
            //当前第i行的方格状态为 0 0 1 0 ,记录到g数组里为g[i] = 1 1 0 1
            //之所以反着记录是因为待会要判断一下当前土地是否可以种植玉米     
            //直接一位一位比较土壤状况和种植情况太过麻烦
            //将该行土壤状态的相反情况和该行的玉米种植情况进行&运算
            //如果 = 0说明没有冲突,否则有冲突
        }

    //对所有行状态预处理,把合法状态记录下来 
    for (int st = 0; st < (1<<m); st ++)
        if (check(st)) 
            state.push_back(st);
            
    //枚举所有行状态,看看哪些状态和哪些状态之间可以转移
    for (int x : state)
        for (int y : state)
            if (!(x & y)) head[x].push_back(y);

    f[0][0] = 1;//初始化
    //因为最后要输出所有的方案数,采用一个小技巧,就是我们在枚举i的时候,枚举到n+1行就可以了
	//假设我们的棋盘是一个n+1 * m的一个棋盘,多了一行,但是最后一行什么都不放
	//所以这里的n最大是13,下标是13的话,数组当然就要开14了,对应上面的N = 14
    for (int i = 1; i <= n + 1; i ++)
        for (int j : state)//枚举一下第i行所有的状态
            if (!(j & g[i]))//如果当前种玉米的状态和题目给出的土壤状态没有冲突
                for (int k : head[j])//枚举能转移到j状态的所有状态(也就是枚举上一行的状态)
                    f[i][j] = (f[i][j] + f[i - 1][k]) % mod;
                    
    //输出第n + 1行状态为0的情况
    cout << f[n + 1][0] << endl;

    return 0;
}

涂抹果酱

在这里插入图片描述
在这里插入图片描述

#include <iostream>
#include <vector>

using namespace std;

const int N = 1e5 + 10, M = 310, mod = 1e6;
typedef long long LL;

int n, m, k;
int f[N][M];
//f[i][j]表示已经考虑完前i行,且第i行状态是j(同时所有状态的第1行都从题目所给的第k行的状态转移而来)的方案个数
int c[10] = {1};//c[i]表示3的i次方
vector<int> state, head[M];//记录所有合法状态   记录每个状态能转移到的状态

//取出x的第k位 等同于二进制下的 x >> k & 1  个位也就是最后一位为第0位
inline int get_k(int x, int k) {return x % c[k + 1] / c[k];}

//判断当前行状态为st时,该行状态是否合法
inline bool check1(int st)
{
    for (int i = 0; i < m - 1; i ++)
        if (get_k(st, i) == get_k(st, i + 1))//相邻区域的果酱不能一样
            return false;
    return true;
}

//判断相邻两行的状态x和y是否冲突
inline bool check2(int x, int y)
{
    for (int i = 0; i < m; i ++)
        if (get_k(x, i) == get_k(y, i))//相邻两行的同一位置要保证果酱不同
            return false;
    return true;
}

int main()
{
    cin >> n >> m >> k;

    int t = 0;//t用来记录第k行的状态
    for (int i = 0; i < m; i ++)
    {
        int x; cin >> x;
        t = t * 3 + x - 1;//将t转化成一个三进制数
    }
    for (int i = 1; i <= 5; i ++) c[i] = c[i - 1] * 3;//预处理3的i次方

    for (int st = 0; st < c[m]; st ++)//枚举所有状态 等同于二进制中的 i < st < (1<<m)
        if (check1(st)) 
            state.push_back(st);    

    //和二进制状态压缩基本差不多
    for (int a : state)
        for (int b : state)
            if (check2(a, b)) head[a].push_back(b);

    f[0][t] = 1;//初始化 第一行的所有状态只能从t状态转移而来
    int x = max(k - 1, n - k), y = min(k - 1, n - k);
    for (int i = 1; i <= x; i ++)
        for (int a : state)
            for (int b : head[a])
                f[i][a] = (f[i][a] + f[i - 1][b]) % mod;

    int ans1 = 0, ans2 = 0;
    for (int st : state) ans1 = (ans1 + f[x][st]) % mod, ans2 = (ans2 + f[y][st]) % mod;
    cout << (LL)ans1 * ans2 % mod << endl;

    return 0;
}

炮兵阵地

在这里插入图片描述

#include <iostream>
#include <vector>

using namespace std;

const int N = 110, M = 1 << 10;

int n, m;
int g[N], cnt[M];//地图  cnt[i]表示i状态下炮兵部队的数量
int f[2][M][M];//普通数组占空间太大,超过题目要求内存限制,改用滚动数组 滚动数组就是 &1 就行
vector<int> state, head[M];

//同一行的炮兵部队直接距离必须大于2
inline bool check(int st) {return !(st & st >> 1 || st & st >> 2);}

inline int count(int st)//当前行部署了多少炮兵部队
{
	int res = 0;
	while (st) res += st & 1, st >>= 1;
	return res;
}

int main() 
{
	cin >> n >> m;
	for (int i = 1; i <= n; i ++)
		for (int j = 0; j < m; j ++)
		{
			char c; cin >> c;
			g[i] += (c == 'H') << j;//不能放炮兵的位置记为1
		}
		
	for (int st = 0; st < (1<<m); st ++)
		if (check(st))
		{
			state.push_back(st);
			cnt[st] = count(st);
		}
	
	//找出能转移到当前状态的上一个状态
	for (int cur_st : state)//枚举当前状态
		for (int pre_st : state)//枚举上一行状态                       0 1 0 0 1
			if (!(cur_st & pre_st))//如果炮兵布置在相邻两行的同一列   比如1 0 0 0 1就不行
				head[cur_st].push_back(pre_st);//说明可以转移,记录下来
	
	for (int i = 1; i <= n + 2; i ++)//一样是小技巧,直接枚举到第n+2行,然后输出f[n+2&1][0][0]
		for (int st : state)//枚举当前行状态
			if (!(g[i] & st))//和题目给的地图没有冲突,(山地上不能够部署炮兵部队)
				for (int p1 : head[st])//枚举上一行
					for (int p2 : head[p1])//枚举上一行的上一行
					//注意:这时还要再判断一下st和p2 这两行也不能有炮兵布置在同一个位置
						if (!(st & p2))//滚动数组大概意思就是奇偶切换,慢慢悟吧
						{
						    int &t = f[i & 1][st][p1];
						    t = max(t, f[i - 1 & 1][p1][p2] + cnt[st]); 
					    //f[i - 1 & 1][p1][p2] + cnt[st])表示前i-1行炮兵数量+当前行st的炮兵数量
						}
						    
	
	cout << f[n + 2 & 1][0][0];
	
	return 0;
}

动物园

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

const int N = 10010, M = 35;

int n, m;
int f[N][M], d[N][M];
//f[i][j]表示枚举到第i个围栏且[i,i+4]的围栏移走状态为j时的最多高兴人数
//d[i][j]表示[i,i+4]这个区间内的围栏移走情况为j时高兴的小朋友数量
int ans;

template <class T>
inline void read(T &res)
{
    char ch; bool flag = false;
    while ((ch = getchar()) < '0' || ch > '9')
        if (ch == '-') flag = true;
    res = (ch ^ 48);
    while ((ch = getchar()) >= '0' && ch <= '9')
        res = (res << 1) + (res << 3) + (ch ^ 48);
    if (flag) res = ~res + 1;
}

int main()
{
    read(n), read(m);
    for (int i = 1; i <= m; i ++)
    {
        int a, b, c; 
        read(a), read(b), read(c);
        int like = 0, fear = 0;//当前小朋友可以看到的围栏中对动物的喜欢和害怕的情况
        for (int j = 1; j <= b; j ++)
        {
            int k; read(k);//k表示当前围栏是从a围栏开始的第几个围栏
            k = (k - a + n) % n;//取模是为了处理环
            fear |= 1 << k;
        }
        for (int j = 1; j <= c; j ++)
        {
            int k; read(k);
            k = (k - a + n) % n;
            like |= 1 << k;            
        }
        for (int st = 0; st < 32; st ++)//遍历从a围栏开始移走围栏的所有状态
            if ((st & fear) || (~st & like))
                d[a][st] ++;
    }
    
    //起始的[1,5]区间的围栏状态是由[0,4]区间转移而来的,所以要枚举一下[0,4]的状态
    for (int i = 0; i < 32; i ++)//遍历[0,4]区间围栏的所有移走状态
    {
        memset(f, -0x3f, sizeof f);//初始化
        f[0][i] = 0;//初始化
        for (int j = 1; j <= n; j ++)//枚举所有起点
            for (int st = 0; st < 32; st ++)//枚举[j,j+4]该区间内围栏的状态
                f[j][st] = max(f[j-1][(st&15)<<1], f[j-1][(st&15)<<1|1]) + d[j][st];
        //f[n][i]即为这一轮当中高兴的小朋友的数量
        ans = max(ans, f[n][i]);
    }
    printf("%d\n", ans);
    
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值