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 的一般特征。
必备

例题: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;状态转移

浙公网安备 33010602011771号