写状压的时候别想其他事情,容易写错!

交换 : 为 和 的内容交换

[位运算]

|  只要有 1 ,就是 1
~  取反
&  都是 1 的时候才是 1
^  不同时才是 1

棋盘式 - 网格连通性

通常为给出一个 大小的棋盘,我们使用 1 和 0 表示格子占用情况,使用二进制的整数表示一行的格子占用情况 ,最后动态向下面一行转移

P1896 [SCOI2005] 互不侵犯

题意
在 的棋盘上,摆放 个国王,保证互相之间不会攻击,有多少种方案.
注意: 一个国王会攻击周围八个点的国王

,

题解
使用状压,记录每一行的摆放方式

#include <bits/stdc++.h>
using namespace std;
#define endl "\n"
#define int long long
#define rep(i, a, b) for(int i = a; i <= b; i ++)
#define per(i, a, b) for(int i = a; i >= b; i --)
#define vi vector<int>
#define vii vector<vi>
#define viii vector<vii>
#define pii pair<int,int>
//#define lc (p << 1)
//#define rc ((p << 1) + 1)
const int INF = 1e18;
const int N = 5e6 + 10;  
//const int mod = 1e9 + 7;
//const int offset = N;
 
void solve(){
    int n, k; cin >> n >> k;
    vi type;
    vi cnt;
    int t_max = 2;
    rep(i, 1, n - 1){
        t_max *= 2;
    }
    t_max -= 1; //最多摆放的情况
    //cout << t_max << endl;
    rep(i, 0, t_max){
        if((((i >> 1) | (i << 1)) & i) != 0) continue; //遍历每一种摆放情况,可能存在本身行之间就矛盾的情况
        int num = 0; 
        int x = i;
        while(x){
            int tmp = x % 2;
            x /= 2;
            if(tmp){
                num ++; //这是记录每个情况下的 1 的个数, 其实也可以使用 >> j & 1?
            }
 
        }
        //if(i == 2) cout << "no" << endl;
        if(num > k) continue;
        //cout << i << endl;
        type.push_back(i);
        cnt.push_back(num);
    }
 
    int m = type.size() - 1;
    viii dp(n + 1, vii(k + 1, vi(m + 1)));
    dp[0][0][0] = 1;
    rep(i, 1, n){
        rep(j, 0, m){
            int c = cnt[j]; 
            int s = type[j];
 
            rep(l, 0, m){
                int c2 = cnt[l]; //这次要摆多少个
                int s2 = type[l]; 
                rep(p, 0, k - c){
                    if(p < c2) continue;
                    if(((s >> 1 | s | s << 1) & s2) != 0) continue;
                    dp[i][p][j] += dp[i - 1][p - c2][l];
                }
            }
        }
    }
 
    int res = 0;
 
    rep(i, 0, m){
        if(k - cnt[i] < 0) continue;
        res += dp[n][k - cnt[i]][i];
        //cout << dp[n][k - cnt[i]][i] << endl;
    }
    cout << res << endl;
    return;
}
signed main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
    //cin >> T;
    while(T--) solve();
    return 0;
}

集合

集合类型的状压 通常表现为一个元素的选或不选,使用二进制整数表示一个集合内元素的选择情况
注: 分为可重复选和不可重复选两种情况

[最短 Hamilton 路径]

题意
给定一张 个点的带权无向图,点从 标号,求起点 到终点 的最短 路径
路径的定义是 到 不重不漏地经过每个点恰好一次

保证直接走到比绕路更短

题解
从走过一个点遍历到走过 个点
需要注意的是必须从 1 出发,必须最终走到 号点

#include <bits/stdc++.h>
using namespace std;
#define endl "\n"
#define int long long
#define rep(i, a, b) for(int i = a; i <= b; i ++)
#define per(i, a, b) for(int i = a; i >= b; i --)
#define vi vector<int>
#define vii vector<vi>
#define viii vector<vii>
#define pii pair<int,int>
//#define lc (p << 1)
//#define rc ((p << 1) + 1)
const int INF = 1e18;
const int N = 5e6 + 10;  
const int mod = 1e8;
//const int offset = N;
 
//int n, m;
void solve(){
    int n; cin >> n;
    vii a(n + 1, vi(n + 1));
    vi b(n + 1);
    b[1] = 1;
    rep(i, 2, n){
        b[i] = b[i - 1] * 2;
    }
    rep(i, 1, n)
        rep(j, 1, n) cin >> a[i][j];
 
    vi type;
    int t_max = 1;
    rep(i, 1, n){
        t_max *= 2;
    }
    //cout << t_max << endl;
    t_max -= 1;
    vi G[n + 1];
    rep(i, 0, t_max){
        if((i & 1) == 0) continue;
        //cout << i << endl;
        int x = i;
        int cnt = 0;
        while(x){
            if(x % 2) cnt ++;
            x /= 2;
        }
        G[cnt].push_back(i);
        type.push_back(i);
    }
 
    vii dp(n + 1, vi(t_max + 1, INF));
    
    int sum = INF;
    dp[1][1] = 0; //到 1 的付出为 0 ,从 1 开始出发
    //从 cnt 开始走吧?
    rep(i, 1, n - 1){
        //cout << endl << endl << i << endl;
        for(auto v : G[i]){
            rep(j, 1, n - 1){
                if((b[j] & v) == 0) continue;
                rep(k, 1, n){
                    if(i != n - 1 && k == n) continue;
                    if((b[k] & v) != 0) continue;
                    //cout << j << " " << v << endl;
                    dp[k][v + b[k]] = min(dp[k][v + b[k]], dp[j][v] + a[j][k]);
                    //cout << k << " " << v + b[k] << endl;
                    //cout << dp[k][v + b[k]] << endl;;
                }
            }
        }
    }
    cout << dp[n][t_max] << endl;
}
 
 
signed main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
    //cin >> T;
    while(T --) solve();
    return 0;
}

售货员的难题

非常典型的通过二进制表示选与不选

徒競走

题意
一个有向无环图,有 个点, 条边,计算拓扑排序种类个数

题解
整个二维的涂色板块可以想象成一个有向图(也就是拓扑排序),在一个板块左上方的涂完了之后,这个板块才能涂色,和上一个 售货员的难题 这道题目不同点就在于这里

通过集合表示已经出圈的点,遍历集合,遍历下一个出圈的点,需要注意此时这个点还没有在集合中表示出圈,但是它的前驱节点必须在集合中表示出圈

可以通过预先处理出圈个数,进行优化

#include <bits/stdc++.h>
using namespace std;
#define int long long //忘记删unsigned毁了我
#define endl "\n"
#define rep(i, a, b) for(int i = a; i <= b; i++)
#define per(i, a, b) for(int i = a; i >= b; i--)
#define vi vector<int>
#define vii vector<vi>
#define viii vector<vii>
#define pii pair<int,int>
//#define lc p<<1
//#define rc (p<<1) + 1
const int N = 2e6 + 10;
const int M = 5e5 + 10;
const int INF = 1e18;
int mod = 998244353;
 
void solve(){
    int n, m; cin >> n >> m;
    //将每个点的前驱节点作为一个集合,所以必须要这些点全都是 1 了,这个点才能被触发,所以
    vi G[n + 1];
    rep(i, 1, m){
        int x, y; cin >> x >> y;
        G[y - 1].push_back(x - 1); //和二进制对齐 
    }
    //根据走了多少点了进行转移?
    
    int t_max = 0;
    rep(i, 0, n - 1){
        t_max += (1 << i);
    }
 
    vi G1[n + 1];
    rep(i, 0, t_max){
        int cnt = 0; 
        rep(j, 0, n - 1){
            if(((i >> j) & 1) != 0) cnt ++;
        }
        G1[cnt].push_back(i); 
    }
 
    vii dp(t_max + 1, vi(n + 1));
    rep(i, 0, n - 1){ 
        //cout << i << endl;
        for(auto k : G1[i]){
            rep(j, 0, n - 1){ //j是下一个要的点
                if(((k >> j) & 1) != 0) continue; //说明这种情况这个点是已经走到过了 
                int g = 0;
                for(auto p : G[j]){
                    //cout << j << " " << p << endl;
                    if(((k >> p) & 1) == 0){ //j 的前驱节点还没有全部推出
                        g = 1; break;
                    }
                }
                if(g) continue; 
                if(i == 0){dp[1 << j][j] = 1; continue;}
                rep(q, 0, n - 1){
                    //cout << k << " " << q << endl;
                    //cout << dp[k][q] << endl;
                    if(((k >> q) & 1) != 0) dp[k + (1 << j)][j] += dp[k][q];
                    //cout << dp[k + (1 << j)][j] << endl;
                }
            }
        }
    }
    int res = 0;
    rep(i, 0, n - 1){
        res += dp[t_max][i];
    }
    cout << res << endl;
}   
 
signed main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
    //cin>>T;
    while(T--) solve();
    return 0;
}

平板涂色

有了上一道拓扑排序之后,这道题就变成了拓展题

题意
只有一把刷子,每次切换颜色都要重新拿起刷子,现在一个平面图上有 个板块,每个板块有规定要涂的颜色,要求必须一个板块上方紧靠着的板块涂完色了,这个板块才能涂色

题解
题目要求必须要一个板块上方紧靠的板块涂完色了才能涂这个板块,所以先找到每个板块的前驱板块,然后转移方程就可以了

#include <bits/stdc++.h>
using namespace std;
#define int long long //忘记删unsigned毁了我
#define endl "\n"
#define rep(i, a, b) for(int i = a; i <= b; i++)
#define per(i, a, b) for(int i = a; i >= b; i--)
#define vi vector<int>
#define vii vector<vi>
#define viii vector<vii>
#define pii pair<int,int>
//#define lc p<<1
//#define rc (p<<1) + 1
const int N = 2e6 + 10;
const int M = 5e5 + 10;
const int INF = 1e18;
int mod = 998244353;
 
void solve(){
    //由于为上方的染完了就能染色,所以我只记录上方就好了
    int n; cin >> n;
    vector<pii> a(n + 1);
    vector<pii> b(n + 1);
    vi c(n + 1);
    vi G[n + 1]; //记录前驱节点
    rep(i, 0, n - 1) cin >> a[i].first >> a[i].second >> b[i].first >> b[i].second >> c[i]; 
 
    rep(i, 0, n - 1){
        rep(j, 0, n - 1){
            if(i == j) continue;
            if(b[j].first == a[i].first && a[j].second < b[i].second && b[j].second > a[i].second){
                G[i].push_back(j); //前驱节点
                //cout << i << "  " << j << endl;
            }
        }
    }
 
    int t_max = 0;
    rep(i, 0, n - 1) t_max += (1 << i); 
    vii dp(t_max + 1, vi(n + 1, INF));
    //遍历涂色情况,遍历下一步涂色板块,遍历上一步涂色板块,上一步的涂色板块是否有要求?,一定是INF,所以可以忽略 
    rep(i, 0, t_max){
        rep(j, 0, n - 1){
            if((i >> j) & 1) continue;
            int g = 0;
            for(auto p : G[j]){
                if(((i >> p) & 1) == 0){g = 1; break;}
            }
            if(g) continue;
            if(i == 0) dp[1 << j][j] = 1; //第一次不需要方程转移
            rep(q, 0, n - 1){
                if((i >> q) & 1){
                    int tmp = 0;
                    if(c[q] != c[j]) tmp = 1;
                    dp[i + (1 << j)][j] = min(dp[i][q] + tmp, dp[i + (1 << j)][j]);
                }
            }
        }
    }
 
    int res = INF;
    rep(i, 0, n - 1){
        res = min(res, dp[t_max][i]);
    }
    cout << res << endl;
    return;
}   
 
signed main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
    //cin>>T;
    while(T--) solve();
    return 0;
}

懒得打乱

题意
一个从 1 到 的排序序列 , 根据 重新打乱,计算两个逆序对数量差最大时序列 的排序类型
答案对 998244353 进行取模

题解
假设从原序列到打乱后的序列一共改变了 对的相对顺序,那么想要让差值最大,就要求,这 对要么全都是逆序对,要么全都不是逆序对

根据对称性,求一种情况就可以,所以可以求非逆序对的时候,那么比它小的点就是它的前驱节点,变成一个前驱集合,按照拓扑排序的做法就可以了

#include <bits/stdc++.h>
using namespace std;
//#define int long long 
#define endl "\n"
#define rep(i, a, b) for(int i = a; i <= b; i++)
#define per(i, a, b) for(int i = a; i >= b; i--)
#define vi vector<int>
#define vii vector<vi>
#define viii vector<vii>
#define pii pair<int,int>
//#define lc p<<1
//#define rc (p<<1) + 1
const int N = 2e6 + 10;
const int M = 5e5 + 10;
const int INF = 1e18;
int mod = 998244353;
 
void solve(){
    int n; cin >> n;
    vi a(n + 1);
    //求在 p 序列中的逆序对,最后排数字的时候,直接算都是非逆序对数量,最后 * 2
    rep(i, 0, n - 1){cin >> a[i]; a[i] -= 1;}
    int cnt = 0;
    vi G[n + 1];
    rep(i, 0, n - 1){
        rep(j, i + 1, n - 1){
            if(a[i] > a[j]){ G[j].push_back(i); cnt ++;}
        }
    }
 
 
    int t_max = 0;
    rep(i, 0, n - 1) t_max += (1 << i);
    //cout << t_max << endl;
 
    vi dp(t_max + 1);
    //预处理个数?
 
    vi t;
    t.push_back(0);
    rep(i, 1, t_max){
        int g = 0;
        rep(j, 0, n - 1){
            if(((i >> j) & 1) == 0) continue;
            for(auto k : G[j]){
                if(((i >> k) & 1) == 0){
                    g = 1; break;
                }
            }
            if(g) continue;
        }
        if(g == 0) t.push_back(i);
    }
 
 
    //遍历已经放置的类型, 遍历下一个要放置的点,遍历上一个放置的点
    rep(w, 0, t.size() - 1){
        int i = t[w];
        rep(j, 0, n - 1){
            if(((i >> j) & 1)) continue; //说明这个点已经放置了
            int g = 0;
            for(auto p : G[j]){
                if(((i >> p) & 1) == 0) {g = 1; break;}
            }
            if(g) continue;
            if(i == 0) dp[1 << j] = 1;
            else{
                dp[i + (1 << j)] += dp[i];
                dp[i + (1 << j)] %= mod;
            }
        }
    }
    int res = 0;
    //cout << res << endl;
 
    res = (res + dp[t_max]) % mod;
    
    //如果没有逆序对的话,就不能 * 2 了,所以需要记录有没有逆序对
    cout << (cnt == 0? res : res * 2 % mod) << endl;
    return;
    
}   
 
signed main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
    //cin>>T;
    while(T--) solve();
    return 0;
}

组队

题意
一共 名队员,三个人一个队,每两个人之间都有一个默契值,一个队的得分是三个人两两之间的默契值之和,计算所有队伍得分总和的最大值

题解
最大为 24 名成员,所有选择使用状压 , 如果只是普通的状压,时间复杂度会达到
所以考虑优化到 之内

优化方向:
从左往右选队伍,也就是相当于如果一个队伍里面编号最小的作为队长,队长去两个队员,左边第一个 0 的作为新的队伍的队长

#include <bits/stdc++.h>
using namespace std;
#define int long long 
#define endl "\n"
#define rep(i, a, b) for(int i = a; i <= b; i++)
#define per(i, a, b) for(int i = a; i >= b; i--)
#define vi vector<int>
#define vii vector<vi>
#define viii vector<vii>
#define pii pair<int,int>
//#define lc p<<1
//#define rc (p<<1) + 1
const int N = 2e5 + 10;
const int M = 5e5 + 10;
const int INF = 1e18;
int mod = 998244353;
 
void solve(){
    int n; cin >> n;
    vii a(3 * n, vi(3 * n));
    rep(i, 0, 3 * n - 1){
        rep(j, 0, 3 * n - 1) cin >> a[i][j];
    }
 
    //使用三个 1 表示一个队伍, 0 - 3 * n - 1
    vi tmp[3 * n + 1]; //记录第一个
    vi f;
    int t_max = 0;
    rep(i, 0, 3 * n - 1) t_max += (1 << i);
    vi dp(t_max + 1, -INF);
    dp[0] = 0;
    rep(i, 0, 3 * n - 3){
        rep(j, i + 1, 3 * n - 2){
            rep(k, j + 1, 3 * n - 1){
                tmp[i].push_back((1 << i) + (1 << j) + (1 << k)); //每次选出 3 个人 
                dp[(1 << i) + (1 << j) + (1 << k)] = (a[i][j] + a[i][k] + a[j][k]); //三个组队的
            } 
        }
    }
 
    vi G[3 *n + 1];
    //int x = 0;
    rep(i, 0, t_max){
        int cnt = 0;
        rep(j, 0, 23){
            if(((i >> j) & 1)){
                cnt ++;
            }
        }
        if(cnt % 3) continue;
        int g = 0;
        rep(j, 0, cnt / 3 - 1){
            if(((i >> j) & 1) == 0){g = 1; break;}
        }
        if(g == 1) continue;
        G[cnt].push_back(i);
        //x ++;
    }
    //cout << x << endl;
    
    for(int c = 3; c <= 3 * n; c += 3){ 
        for(auto i : G[c]){ 
            rep(j, 0, 3 * n - 1){
                if(((i >> j) & 1)){continue;}
                for(auto k : tmp[j]){
                    if(((k & i) != 0)) continue;
                    dp[k | i] = max(dp[k | i], dp[i] + dp[k]);
                }
                break;
            }
        }
    }
    cout << dp[t_max] << endl;
    
    return;
}   
 
signed main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
    //cin>>T;
    while(T--) solve();
    return 0;
}

轮廓线 (插头 / 折线)

适用情况:
无法整行或者整列表示转移的棋盘类型

与直接的棋盘式(连通型)不同的是,轮廓线 是一个格子一个格子转移,每次的状态是一个线形的

例题:
蒙德里安的梦想

这道题有两种解法:
1.棋盘式,使用 1 和 0 表示这个格子被竖着覆盖的还是横着覆盖的,然后状态转移
2.轮廓线

#include <bits/stdc++.h>
using namespace std;
#define int long long 
#define endl "\n"
#define rep(i, a, b) for(int i = a; i <= b; i++)
#define per(i, a, b) for(int i = a; i >= b; i--)
#define vi vector<int>
#define vii vector<vi>
#define viii vector<vii>
#define pii pair<int,int>
//#define lc p<<1
//#define rc (p<<1) + 1
const int N = 2e6 + 10;
const int M = 5e5 + 10;
const int INF = 1e18;
int mod = 998244353;
 
void solve(){
    int n, m;
    while(cin >> n >> m){
        if(n == 0 && m == 0) break;
        
        //遍历格子,遍历上一个格子的状态,转移,如果合法,就转移
        //状态表示为 m + 1 位
        int t_max = 0;
        rep(i, 0, m) t_max += (1 << i); //全部填完的状态
 
        vi dp(t_max + 1); //记录上一个的状态
        dp[0] = 1;
        rep(i, 0, n - 1){
            rep(j, 0, m - 1){
                vi ndp(t_max + 1);
                rep(k, 0, t_max){ //上一个方块的状态
                    if((k >> 1) & 1){
                        ndp[k >> 1] += dp[k];
                    }
                    if(j < m - 1 && ((k >> 1) & 1) == 0 &&((k >> 2) & 1) == 0){ //需要保证这个点不是整行的最后一个
                        ndp[(k >> 1) | 3] += dp[k];
                    }
 
                    if(i < n - 1 && ((k >> 1) & 1) == 0){
                        ndp[(k >> 1) | 1 | (1 << m)] += dp[k];
                    }
                }
                ndp.swap(dp);
            }
        }
        cout << dp[1] << endl;
    }
    return;
}   
 
signed main(){
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int T = 1;
    //cin>>T;
    while(T--) solve();
    return 0;
}