O - Matching AT_DP 状态压缩DP

经典状态压缩DP

简单介绍这种DP

利用位运算,将一个集合的状态压缩成一个整数来表示,并以此作为 DP 的状态维度。
例子1
在普通的 DP 中,我们的状态可能是 dp[i][j] 表示坐标。但在某些问题中,状态涉及到“一组元素的集合”
例如:有N个城市,我们需要记录哪些城市已经去过,哪些还没去过。
如果用数组 bool visited[20] 来记录,这个数组没法直接作为 DP 数组的下标。
如果我们想把这 20 个城市的状态都存进 DP,状态维度会变成 dp[2][2][2]...[2](20个2),这在编程中极难实现。
例子2
核心原理:二进制表示集
假设有 3 个物品,状态可以表示为:
000 (0):什么都不选
001 (1):只选第 0 个
101 (5):选了第 0 个和第 2 个
111 (7):全选

适用场景

1.数据范围小,比如20这样的
2.涉及集合覆盖/排列:问题往往关于“如何安排一套元素的顺序”或“如何完全覆盖一个网格”。
3.具备最优子结构和重叠子问题:符合 DP 的一般特征。

必备

屏幕截图 2026-04-01 131152

例题:ATC的DP题单里的O - Matching

https://atcoder.jp/contests/dp/tasks/dp_o?lang=en
求能凑成几个n对,在每个n对中,男和女都只能出现一次

3
0 1 1
1 0 1
1 1 1
简单来说就是
0 号男喜欢1 和2
1 号男喜欢0和2
2 号男喜欢0 1 2
要求配出三对情侣(应该是情侣吧)且不可以有脚踏两只船的情况出现(违反公序良俗
(1,2),(2,1),(3,3)
(1,2),(2,3),(3,1)
(1,3),(2,1),(3,2)
经过上面的介绍之后,就会发现这题未免太状态压缩dp了

code

#include<bits/stdc++.h>
using namespace std;
//"O campeão tem nome, e se chama Charles Oliveira!"
#define int long long
#define endl '\n'
#define ep emplace
#define pob 
#define ll long long
#define pb push_back
#define pof pop_front
#define pob pop_back
#define all(a) a.begin(),a.end()
#define rall(a) a.rbegin(),a.rend()
#define mod 998244353
#define MOD 1000000007

using ld = long double;
using ui = unsigned;
using ull = unsigned long long;
using i128 = __int128;

void solve(){
    int n;cin>>n;
    vector<vector<int>>a(n,vector<int>(n));
    vector<int>dp(1<<n);
    for(int i=0;i<n;i++){
        for(int j=0;j<n;j++){
            cin>>a[i][j];
        }
    }
    int max_mask=1<<n;
    dp[0]=1;
    for(int mask=0;mask<max_mask;mask++){
        if(!dp[mask])continue;//就是女人们的占有情况不可能达到这种情况,那何必去计算
        int i=0;
        for(int b=0;b<n;b++){
            if((mask>>b)&1)i++;
        }
        for(int j=0;j<n;j++){
            if(!((mask>>j)&1)&&a[i][j]==1){
                int next_mask=mask|(1<<j);
                dp[next_mask]=(dp[mask]+dp[next_mask])%MOD;
            }
        }
    }
    cout<<dp[max_mask-1]<<endl;
}

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    int t=1;
    //cin>>t;
    while(t--)solve();
}

代码分析

为什么是这样写,或者说为什么用这个方法

假如用最朴素的方法写,就是第一个男人可以有n种选法
第二个男人有n-1种
依此推类,假设n=21,那么就是21!超级大

思路分析

这里的dp的意思是:女生被占用情况如该数字的二进制时,有几种配对方法
我们是用一个二进制数字来表示女生被占用的情况
比如000->0 就是女生一个都没被占用
001->1就是女生中的2号被占用了
然后我们开始枚举,刚开始的时候,dp[0]也就是一个女生都没被占用是,就是1,然后开始枚举0~max_mask-1,如果这个dp[mask]是0,就跳过,为什么?因为没有任何意义,根本不会出现这种女生被占用的情况,你到后面
状态转移的时候,dp[next_mask]=(dp[mask]+dp[next_mask])%MOD;这个的dp[mask]==0,那这个式子还有什么意思呢?这样也可以提高代码的效率

int i=0;
        for(int b=0;b<n;b++){
            if((mask>>b)&1)i++;
        }

这个是比较巧妙的
我们根本不需要去记录到第几个男生了,这段代码的记录的是这个数字中有几个1,有几个1就代表这个有几个女生已经被选走了,也就是现在到第几个男生了,注意这里有个小细节b是从0开始的,也就是说假设n是3,那么i最多就到2,所以到第三个男生的时候,i其实是2,这样我们的a[i][j]才不会超过边界

for(int j=0;j<n;j++){
            if(!((mask>>j)&1)&&a[i][j]==1){
                int next_mask=mask|(1<<j);
                dp[next_mask]=(dp[mask]+dp[next_mask])%MOD;
            }
        }

枚举位数0~n-1,当移动到某位,某个位置是0,就代表这个女生是空闲的,如果a[i][j]是1的话,代表这个女生可以跟这个男生配对
int next_mask=mask|(1<<j);这个的意思就是构造一个新的数字,在这个数字里原来女生的空闲情况不变,但是刚刚配对的女生要从0变成1
101|010=111,然后dp[next_mask]=(dp[mask]+dp[next_mask])%MOD;状态转移

posted @ 2026-04-01 16:57  Time_q  阅读(3)  评论(0)    收藏  举报