状压DP I
前言
状压DP其实就是普通DP加上用二进制进行状态压缩,难点应该在于如何压缩状态与找到状态间的关系(更难的恐怕就是得知道这道题是状压DP了把 QAQ)
紫书的题太难,暂时不上紫书的题
- 状压DP
https://hrbust-acm-team.gitbooks.io/acm-book/content/dynamic_programming/state.html - 状压DP入门题 POJ - 3254
https://www.cnblogs.com/Ronald-MOK1426/p/8456945.html
https://blog.csdn.net/harrypoirot/article/details/23163485
Harry And Dig Machine - HDU - 5067
题意
TSP问题,求从点(0,0)出发到所有非0点后回到(0,0)点的最短距离
思路 —— TSP问题
定义dp状态dp[st][i]
表示到达状态st(st代表已经经过的点的集合)且i为最后一个到达的点时所走过的最小路径长度,则dp[st][i] = min{dp[st^i][j] + dist[j][i], j ∈ st^i}
,即能走到st状态的dp值加上该点到i的距离,其中取最小值
由于走的是环路,所以需要额外处理一下边界值 dp[{i}][i] = dist[0][i]
,即先从0出发到点i,但是0点不计入已访问的点
最后的答案就是 dp[{0,1,2,...,n-1}][0]
即 dp[(1 << n) - 1][0]
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 11 + 1;
const int MOD = (int)1e9;
typedef long long ll;
const int inf = 0x3f3f3f3f;
int dp[1 << N][N];
int x[N], y[N], pp;
int dist[N][N];
inline int mabs(int x){ return x < 0 ? -x : x; }
int main(){
int n, m;
while(~scanf("%d%d", &n, &m)){
memset(dp, 0x3f, sizeof(dp));
memset(dist, 0, sizeof(dist));
pp = 0;
x[pp] = y[pp] = 0;
pp++;
for(int i = 0; i < n; i++){ //为了计算dist,存下x和y坐标
for(int j = 0; j < m; j++){
int tmp;
scanf("%d", &tmp);
if(tmp){
x[pp] = i, y[pp] = j;
pp++;
}
}
}
for(int i = 0; i < pp; i++){ //计算dist
for(int j = i + 1; j < pp; j++){
dist[i][j] = dist[j][i] = mabs(x[i] - x[j]) + mabs(y[i] - y[j]);
}
}
for(int i = 0; i < pp; i++){
dp[1 << (pp - i - 1)][i] = dist[0][i];
}
for(int st = 1; st < (1 << pp); st++){
for(int i = 0; i < pp; i++){
if((st & (1 << (pp - i - 1))) == 0) continue; //点i不在st中说明是非法状态
int prest = st^(1 << (pp - i - 1)); //可以走到i的状态是将i的对应为置0
for(int j = 0; j < pp; j++){
if((prest & (1 << (pp - j - 1))) == 0) continue;
dp[st][i] = min(dp[st][i], dp[prest][j] + dist[j][i]);
}
}
}
printf("%d\n", dp[(1 << pp) - 1][0]);
}
}
Ingress - HDU - 5711
题意
求从点(0,0)出发回到点(0,0),经过每个点可以hack若干次,第一次会获得a[i]分,随后每一次得到的分数都会下降b[i]分,直到为0,求在走的总路径不超过l,总hack次数不超过k的情况下,得分最大值
思路 —— TSP问题
和上题类似,不过如果定义状态dp[st][i][k]
,代表走到i时到达st状态且这个点hack了k次所能得到的最大分数,看上去是没错的,但是会MLE!
于是仍然与上题一样维护长度最小值,最后再用枚举状态,找出dp[{0,..}][0]
dp状态,且该状态下值 < l,再贪心思想,用优先队列优先选k个大的,就可以了
这样做的正确性在于,对于每一个到达0的状态,我们都取了最短路径,因此每一种可能性都顾及到了,而考虑k次hack与如何经过这些状态的点无关,所以直接对每一种可能性贪心即可
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <functional>
using namespace std;
const int N = 17;
const int MOD = (int)1e9;
typedef long long ll;
const int inf = 0x3f3f3f3f;
typedef pair<int, int> pii;
int dp[1 << N][N];
int a[N], b[N];
int dist[N][N];
priority_queue<pii, vector<pii>, less<pii> > que;
void floyd(int n){
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
for(int k = 0; k < n; k++){
dist[i][j] = dist[j][i] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
}
int getVal(int st, int k, int n){
while(!que.empty()) que.pop();
for(int i = 0; i < n; i++){
if(st & (1 << (n - i - 1))) que.push(make_pair(a[i], i));
}
int ret = 0;
while(k--){
pii cur = que.top();
que.pop();
ret += cur.first;
que.push(make_pair(max(0, cur.first - b[cur.second]), cur.second));
}
return ret;
}
int main(){
int t, csn = 1;
scanf("%d", &t);
while(t--){
int n, m, k, l;
scanf("%d%d%d%d", &n, &m, &k, &l);
memset(dp, 0x3f, sizeof(dp));
memset(dist, 0x3f, sizeof(dist));
for(int i = 0; i <= n; i++) dist[i][i] = 0;
a[0] = b[0] = 0;
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
for(int i = 1; i <= n; i++) scanf("%d", &b[i]);
while(m--){
int u, v, c;
scanf("%d%d%d", &u, &v, &c);
dist[u][v] = dist[v][u] = min(dist[v][u], c);
}
floyd(++n);
for(int i = 1; i < n; i++){
if(dist[0][i] > l) continue;
dp[1 << (n - i - 1)][i] = dist[0][i];
}
for(int st = 1; st < (1 << n); st++){
for(int i = 0; i < n; i++){
if((st & (1 << (n - i - 1))) == 0) continue;
int prest = st^(1 << (n - i - 1));
for(int j = 0; j < n; j++){
if((prest & (1 << (n - j - 1))) == 0) continue;
dp[st][i] = min(dp[st][i], dp[prest][j] + dist[j][i]);
}
}
}
int ans = 0;
for(int st = 0; st < (1 << n); st++){
if((st & (1 << (n - 1))) == 0) continue;
if(dp[st][0] > l) continue;
ans = max(ans, getVal(st, k, n));
}
printf("Case %d: %d\n", csn++, ans);
}
}
color II - HDU - 5823
题意
图的染色问题,求对每一个子集染色最少需使用多少种颜色,答案输出 sum(点的集合S染色最少颜色种数*233^(2^u1 + 2^u2 + ...), u ∈ S)
思路 —— 图的染色问题
模板题
按紫书的介绍就是,定义dp状态dp[s]
,代表将集合s染色所需最少颜色种数,则dp[s] = dp[s - subs] + 1
,其中subs是可以染成同一颜色的s的子集(可以是s)
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 18;
typedef long long ll;
const int inf = 0x3f3f3f3f;
int G[N][N];
char s[N];
int dp[1 << N];
bool ok[1 << N];
int main(){
int t;
scanf("%d", &t);
while(t--){
int n;
scanf("%d", &n);
for(int i = n - 1; i >= 0; i--){
scanf("%s", s);
for(int j = n - 1; j >= 0; j--){
G[i][j] = s[n - j - 1] - '0';
}
}
ok[0] = true;
for(int st = 1; st < (1 << n); st++){ //预处理判断是否可以染成同一颜色
ok[st] = true;
for(int i = 0; i < n; i++){
if((st & (1 << (n - i - 1))) == 0) continue;
for(int j = i + 1; j < n; j++){
if((st & (1 << (n - j - 1))) == 0) continue;
if(G[i][j]){
ok[st] = false;
break;
}
}
if(!ok[st]) break;
}
}
dp[0] = 0;
for(int st = 1; st < (1 << n); st++){
dp[st] = inf;
for(int subst = st; subst; subst = st & (subst - 1)){ //高效枚举st的子集
if(ok[subst]) dp[st] = min(dp[st], dp[st - subst] + 1);
}
}
unsigned fac = 233, ans = 0;
for(int st = 1; st < (1 << n); st++){
ans = ans + fac*dp[st];
fac = fac * 233;
}
printf("%u\n", ans);
}
}
Corn Fields - POJ - 3254
题意
给定01矩阵,其中1上面可以种草,0的不行,现在规定相邻土地上不能种草,问有多少种种法(可以全部不种)
思路
首先枚举每一行可行的状态(根据01矩阵)
然后定义dp状态dp[st][i]
,表示种到第i行时前i行的种植方案数量,则dp[st][i] = sum(dp[prest][i - 1])
,其中prest要和st满足上下不相邻
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 400 + 15;
const int MOD = (int)1e9;
typedef long long ll;
int cur[N];
int state[N], pst;
ll dp[N][N];
void initState(){
pst = 0;
for(int i = 0; i < (1 << 12); i++){
if(!(i&(i << 1))) state[pst++] = i;
}
}
int main(){
initState();
int n, m;
while(~scanf("%d%d", &n, &m)){
for(int i = 0; i < n; i++){
cur[i] = 0;
for(int j = 0; j < m; j++){
int tmp;
scanf("%d", &tmp);
if(tmp) cur[i] += (1 << (m - j - 1));
}
}
for(int j = 0; j < pst; j++){
if(state[j]&(~cur[0])) dp[0][j] = 0;
else dp[0][j] = 1;
}
for(int i = 1; i < n; i++){
for(int j = 0; j < pst; j++){
dp[i][j] = 0;
if(state[j]&(~cur[i])) continue;
for(int k = 0; k < pst; k++){
if(state[j]&state[k]) continue;
dp[i][j] = (dp[i][j] + dp[i - 1][k])%MOD;
}
}
}
ll ans = 0;
for(int j = 0; j < pst; j++){
ans = (ans + dp[n - 1][j])%MOD;
}
printf("%lld\n", ans);
}
}
【P1896】[SCOI2005]互不侵犯 - 洛谷
思路
与上一题类似,但是要多加一个维度记录下前i行共摆了多少个King,以及判断与上一行的关系的时候除了要判断上下相邻,还要判断两条对角线相邻
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 9 + 1;
const int MOD = (int)1e9;
typedef long long ll;
ll dp[N][1 << N][N*N];
int state[N*N], pst;
int getCnt(int x){
int ret = 0;
while(x){
x = (x & (x - 1));
ret++;
}
return ret;
}
void init(int n){
pst = 0;
for(int i = 0; i < (1 << n); i++){
if(i & (i << 1)) continue;
if(i & (i >> 1)) continue;
state[pst++] = i;
}
}
int main(){
int n, k;
while(~scanf("%d%d", &n, &k)){
init(n);
memset(dp, 0, sizeof(dp));
for(int i = 0; i < pst; i++){
int st = state[i];
int cnt = getCnt(st);
if(cnt > k) continue;
dp[0][st][cnt] = 1;
}
for(int i = 1; i < n; i++){
for(int p = 0; p < pst; p++){
int st = state[p];
int cnt_st = getCnt(st);
for(int j = cnt_st; j <= k; j++){
for(int q = 0; q < pst; q++){
int prest = state[q];
if(st & prest) continue;
if(st & (prest << 1)) continue;
if(st & (prest >> 1)) continue;
dp[i][st][j] += dp[i - 1][prest][j - cnt_st];
}
}
}
}
ll ans = 0;
for(int i = 0; i < pst; i++){
int st = state[i];
ans += dp[n - 1][st][k];
}
printf("%lld\n", ans);
}
}