其他分享
首页 > 其他分享> > 状压DP题单

状压DP题单

作者:互联网

状态压缩DP


技巧

求一个数的二进制表示中含有多少个 t r u e true true:

int table[16] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};
int popcount(unsigned int x) {
    int ret = 0;
    for (int i = 0; i < 8; i++)
        ret += table[x & 15], x >>= 4;
    return ret;
}

1.互不侵犯

考虑构成: 1 ≤ N ≤ 9 1 \leq N \leq 9 1≤N≤9,所以根据 N N N 构造状压 d p dp dp,将每一行国王的摆放情况进行压缩

考虑状态: 数组 d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k] 表示第 i i i 行国王的摆放情况为 j j j 的情况下,已经摆放了 k k k 个国王

考虑转移: 第 i i i 行的国王摆放状态受到第 i − 1 i - 1 i−1 行国王摆放状态的制约,所以第 i i i 行由第 i − 1 i - 1 i−1 行转移得到

具体做法: 外层枚举行数,内层分别枚举第 i i i 行的摆放情况以及第 i − 1 i - 1 i−1 行的摆放情况,如果满足条件,则还需要考虑对截止到第 i − 1 i - 1 i−1行摆放的国王数量做状态转移(虽然第 i − 1 i - 1 i−1 行的摆放情况确定了,但是截止到第 i − 1 i - 1 i−1 行时,所有摆放的国王数量是不定的,所以对之前的所有情况做状态转移)

条件限制:

示例代码(如有雷同,纯属抄袭):

#include <bits/stdc++.h>
using namespace std;
#define int long long
vector<int> vec;
int sum[1 << 10];
int f[20][20][1 << 10];
int n, m;
int getsum(int x) {
	int cnt = 0;
	while(x) {
		cnt += x & 1;
		x >>= 1;
	}
	return cnt;
}
signed main() {
	cin >> n >> m;
	for(int i = 0; i < 1 << n; i++) {
		sum[i] = getsum(i);
		if( (i & (i << 1)) or (i & (i >> 1)) or sum[i] > m) continue;
		vec.push_back(i);
	}
	for(auto i : vec) {
		f[0][sum[i]][i] = 1; 
	}
	for(int i = 1; i < n; i++) {
		for(auto j : vec) {
			for(auto k : vec) {
				if(j & k) continue;
				if(j & (k << 1)) continue;
				if(j & (k >> 1)) continue;
				for(int s = m; s >= sum[j]; s--) {
					f[i][s][j] += f[i - 1][s - sum[j]][k];
				}
			}
		}
	}
	int ans = 0;
	for(auto i : vec) {
		ans += f[n - 1][m][i]; 
	}
	cout << ans << endl; 
	return 0;
}

2.邦邦的大合唱站队

考虑构成: 由于队伍数量 1 ≤ M ≤ 20 1\leq M \leq 20 1≤M≤20,所以根据 M M M 构造状压 d p dp dp,将所有队伍的排列顺序进行状态压缩

考虑状态: 由于题目要求所有队伍必须相邻,即相当于是对于一段连续的位置,我们依次分配给每个队伍一段位置,长度就是这个队伍的人数,最后的结果就是 ( 1 … 1 … 1 ) 2 (1…1…1)_2 (1…1…1)2​ ,表示所有的队伍都已经分配好了位置,所以

考虑转移: 对于状态 i i i 而言,如果分配给编号 j j j 的队伍位置,那么 [ p o s l , p o s r ] [pos_l, pos_r] [posl​,posr​] 这一段区间位置都应该由队伍 j j j 的队员占领,出队人数应该是这段区间内不属于编号 j j j 的人的数量,利用前缀和计算即可

示例代码:

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int N = 21, M = 100010;
int f[1 << N], sum[M][N], num[N];
int n, m;
signed main() {
    cin >> n >> m;
    for(int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        num[x - 1]++;
        for(int j = 0; j < m; j++)
            sum[i][j] = sum[i - 1][j];
        sum[i][x - 1]++;
    }
    memset(f, 0x3f3f3f, sizeof f);
    f[0] = 0;
    for(int i = 0; i < 1 << m; i++) {
        int length = 0;
        for(int j = 0; j < m; j++)
            if(i & (1 << j))
                length += sum[n][j];
        for(int j = 0; j < m; j++)
            if(i & (1 << j))
                f[i] = min(f[i], f[i ^ (1 << j)] + num[j] - sum[length][j] + sum[length - num[j]][j]);
    }
    cout << f[(1 << m) - 1] << endl;
    return 0;
}

3.砝码称重

考虑构成: 由于砝码数量为 1 ≤ N ≤ 20 1 \leq N \leq 20 1≤N≤20,所以根据砝码数量 N N N 构造状压 d p dp dp,将所有砝码的所有选择情况进行状态压缩

考虑状态: 由于是要求最多能称量出多少种重量,所以我们需要枚举出每一种满足题意的状态,一旦我们知道选择哪几个砝码后,求能称出的重量,就是类似于01背包求方案数的做法。

考虑转移: 考虑到每一次称出的重量都是在上一次的基础上得到的,所以枚举当前已经有的重量,第 i i i 个合法砝码选择后方案数是否能增加,从之前的所有称出的重量的基础上得到

具体做法: 首先可以采用 d f s dfs dfs 或者 f o r for for 循环的方式枚举得到所有的合法状态,对于每一个合法状态都需要进行一次 d p dp dp,在 d p dp dp 过程中,首先要对标记数组 v i s vis vis 初始化,表示是否得到过这个重量,注意 v i s [ 0 ] = 1 vis[0] = 1 vis[0]=1,一开始啥也没有嘛毕竟。之后对于状态 s t a t e state state,枚举砝码的编号 [ 1 , n ] [1, n] [1,n],如果 s t a t e > > ( i − 1 ) & 1 state >> (i - 1) \& 1 state>>(i−1)&1 为 1 1 1 表示这个砝码是被选择了的,之后从所有重量进行状态转移即可。

参考代码:

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
int n, m, ans;
int state[1 << 21], a[30], f[30], vis[2020];
int table[16] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};
int popcount(unsigned int x) {
    int ret = 0;
    for (int i = 0; i < 8; i++)
        ret += table[x & 15], x >>= 4;
    return ret;
}
void dp(int state) {
    memset(vis, 0, sizeof vis);
    vis[0] = 1;
    int tot = 0;
    int cnt = 0;
    for (int i = 0; i < n; i++) {
        if (!(state >> i & 1)) {
            for (int j = tot; j >= 0; j--) {
                if (vis[j] && !vis[j + a[i + 1]]) {
                    vis[j + a[i + 1]] = 1;
                    cnt++;
                }
            }
            tot += a[i + 1];
        }
    }
    ans = max(ans, cnt);
    return;
}
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    for (int i = 0; i < (1 << n); i++)
        if (popcount(i) == m)
            dp(i);
    cout << ans << endl;
    return 0;
}

4.炮兵阵地

考虑构成: 由于列数 1 ≤ M ≤ 10 1\leq M \leq10 1≤M≤10,所以考虑根据每一行的排列情况进行 d p dp dp,将每一行的炮兵部队状态进行压缩

注意事项:

示例代码:

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
int a[1 << 10], n, m, sum[1 << 10], dp[1 << 10][1 << 10][2];
char x;
int getsum(int s) {
	int res = 0;
	while(s) {
		res += (s & 1);
		s >>= 1; 
	}
	return res;
}
int main() {
	scanf("%d %d", &n, &m);
	for(int i = 0; i < n; i++) {
		for(int j = 0; j < m; j++) {
			cin >> x;
			a[i] <<= 1;
			a[i] += (x == 'H' ? 1 : 0);
		}
	}
	for(int s = 0; s < (1 << m); s++) {
		sum[s] = getsum(s);
		if(!((s & a[0]) or (s & (s << 1)) or (s & (s << 2)))) {
			dp[0][s][0] = sum[s];
		}
	}
	for(int l = 0; l < (1 << m); l++) {
		for(int s = 0; s < (1 << m); s++) {
			if(!((l & s) or (l & a[0]) or (s & a[1]) or (l & (l << 1)) or (l & (l << 2)) or (s & (s << 1)) or (s & (s << 2)))) {
				dp[l][s][1] = sum[l] + sum[s];
			}
		}
	}
	for(int i = 2; i < n; i++) {
		for(int l = 0; l < (1 << m); l++) {
			if((l & a[i - 1]) or (l & (l << 1)) or (l & (l << 2))) {
				continue;
			}
			for(int fl = 0; fl < (1 << m); fl++) {
				if((fl & a[i - 2]) or (fl & (fl << 1)) or (fl & (fl << 2)) or (fl & l)) {
					continue;
				}
				for(int s = 0; s < (1 << m); s++) {
					if((s & a[i]) or (s & (s << 1)) or (s & (s << 2)) or (s & fl) or (s & l)) {
						continue;
					}
					dp[l][s][i % 2] = max(dp[l][s][i % 2], dp[fl][l][(i + 1) % 2] + sum[s]);
				}
			}
		}
	}
	int ans = 0;
	for(int l = 0; l < (1 << m); l++) {
		for(int s = 0; s < (1 << m); s++) {
			ans = max(ans, dp[l][s][(n - 1) % 2]);
		}
	}
	cout << ans << endl;
	return 0;
}

5.Corn Fields G

可以看作是[《互不侵犯》]([P2704 NOI2001] 炮兵阵地 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))和《炮兵阵地》 的变式,考虑地形以及相邻两行的种植状态之间的相互约束即可。

示例代码:

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N = 20, mod = 100000000;
int f[N][1 << N];
int x[N][N];
vector<int> state;
int main() {
    int n, m;
    cin >> n >> m;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            cin >> x[i][j];
        }
    }
    for (int i = 0; i < (1 << m); i++) {
        if ((i & (i << 1)) or (i & (i >> 1)))
            continue;
        state.push_back(i);
        f[0][i] = 1;
    }
    for (int i = 1; i < n; i++) {
        for (auto j : state) {      // 当前行状态
            for (auto k : state) {  // 上一行状态
                if (j & k)
                    continue;
                int flag = 0;
                for (int t = 0; t < m; t++) {
                    if (((j >> t) & 1) == 1 and x[i][t] == 0) {
                        flag = 1;
                        break;
                    }
                    if (((k >> t) & 1) == 1 and x[i - 1][t] == 0) {
                        flag = 1;
                        break;
                    }
                }
                if (flag) {
                    continue;
                }
                f[i][j] = (f[i][j] + f[i - 1][k] + mod) % mod;
            }
        }
    }
    long long ans = 0;
    for (auto i : state) {
        int flag = 0;
        for (int j = 0; j < m; j++) {
            if (((i >> j) & 1) == 1 and x[n - 1][j] == 0) {
                flag = 1;
                break;
            }
        }
        if (!flag) {
            ans = (ans + f[n - 1][i] + mod) % mod;
        }
    }
    cout << ans << endl;
    return 0;
}

6.No Change G

考虑构成: k ≤ 16 k \leq 16 k≤16,所以根据 k k k 构造状压 d p dp dp,将所有硬币的状态进行压缩

考虑状态: 数组 p o s [ i ] pos[i] pos[i] 表示 i i i 状态下的硬币可以购买到第几个商品, d p [ i ] dp[i] dp[i] 表示状态 i i i 下的花费

考虑转移: 使用当前硬币的状态一定由使用上一个硬币的状态转移而来

具体做法: 外层循环枚举所有状态,内层枚举每一个硬币,如果是使用到的,则进行一次转移,注意到本题要求了必须依次购买商品,所以可以用一个前缀和维护商品的总花费,之后二分出用当前硬币能够购买到的最后一件商品的编号,如果此时已经能够购买到第 n n n 件商品,直接对花费取最小值,最后判断所有硬币的总额是否大于花费即可。

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N = 1e5 + 10;
int n, m, pay, money;
long long ans = 0x3f3f3f3f3f;
int coins[N], sum[N];
int pos[1 << 17], dp[1 << 17];
int check(int value, int st) {
    int l = st, r = n;
    while (l <= r) {
        int mid = (l + ((r - l) >> 1));
        if (sum[mid] - sum[st - 1] == value)
            return mid;
        else if (sum[mid] - sum[st - 1] > value)
            r = mid - 1;
        else
            l = mid + 1;
    }
    return r;
}
int main() {
    cin >> m >> n;
    for (int i = 1; i <= m; i++) {
        cin >> coins[i];
        money += coins[i];
    }
    for (int i = 1; i <= n; i++) {
        cin >> pay;
        sum[i] = sum[i - 1] + pay;
    }
    for (int i = 0; i < (1 << m); i++) {
        for (int j = 0; j < m; j++) {
            if ((i >> j) & 1) {
                int last = i ^ (1 << j);
                int sum = check(coins[j + 1], pos[last] + 1);
                if (sum > pos[i]) {
                    pos[i] = sum;
                    dp[i] = dp[last] + coins[j + 1];
                }
                if (pos[i] == n) {
                    ans = min(ans, (long long)dp[i]);
                }
            }
        }
    }
    cout << (money - ans < 0 ? -1 : money - ans) << endl;
    return 0;
}

标签:状态,int,sum,状压,++,DP,ans,dp,题单
来源: https://blog.csdn.net/a12311weq/article/details/121421724