1. 程式人生 > 其它 >專題4 - 狀壓dp

專題4 - 狀壓dp

狀壓dp,關鍵在於用01串將過程中的狀態進行壓縮且便於儲存。這邊會涉及到位運算。

由於位運算的優先順序比\(==\)還低,所以記得頻繁打上括號以免不幸。

NC20240互不侵犯King

將每一行的擺放情況用01串來表示,這樣就可以將狀態壓縮。用\(f[i][j][k]\)表示第\(i\)行擺放情況為\(k\)且總共已經擺放了\(j\)個棋子。那麼\(f[i][j][k]+=f[i-1][j-num[k]][s]\),其中\(num[k]\)表示情況為\(k\)時需要的棋子數量。同時\(k\)與\(s\)必須滿足:

\(((k>>1)\&k )==0\)

\((k\&s)==0, ((k>>1)\&s)==0,((k<<1)\&s)==0\)

另外,各個情況需要的棋子數量可以進行預處理。

#include<bits/stdc++.h>
#define fast ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
#define ll long long
#define pb push_back
using namespace std;
const int maxn = 20;
int f[maxn][100][(1<<10)+10];
ll num[(1<<10)+10];
void init()
{
    for(int i=0;i<(1<<10);i++)
    {
        
int m=i; while(m) { if(m%2) num[i]++; m/=2; } } } int main() { int n,k; init(); cin>>n>>k; for(int i=0;i<(1<<n);i++) { if((i&(i<<1))!=0) continue; f[1][num[i]][i]=1; } for(int i=2
;i<=n;i++) { for(int j=0;j<=k;j++) { for(int s=0;s<(1<<n);s++) { if((s&(s<<1))!=0) continue; if(num[s]>j) continue; // cout<<i<<' '<<j<<' '<<s<<'\n'; for(int t=0;t<(1<<n);t++) { if((s&t)==0 && ((s<<1)&t)==0 && ((s>>1)&t)==0) { f[i][j][s]+=f[i-1][j-num[s]][t]; //cout<<i<<' '<<j<<' '<<s<<' '<<' '<<j-num[s]<<' '<<t<<' '<<f[i][j][s]<<'\n'; } } } } } ll ans=0; for(int i=0;i<(1<<n);i++) { //cout<<i<<' '<<f[n][k][i]<<'\n'; ans+=f[n][k][i]; } cout<<ans<<'\n'; }

NC16886 炮兵陣地

上一道題目的加強版,要求左右十字範圍內兩個都不能有友軍,且所有的炮兵都需要站在平原上。

那麼首先我們對整張圖進行狀態壓縮處理,用\(0\)表示平原,用\(1\)表示山地。再對每一排的士兵擺放狀態進行狀態壓縮,\(0\)無\(1\)有。這樣的設定會使得當這兩者與運算結果為\(1\)時是非法的。

用\(f[s][i][j]\)表示第\(s\)行狀態為\(i\),前一行狀態為\(j\)時士兵的最大數量。此時列舉前兩行的狀態\(k\),當三者都合法時,狀態轉移方程為\(f[s][i][j]=max(f[s][i][j],f[s-1][j][k]+num[i])\),其中\(num[i]\)表示\(i\)狀態下有多少個士兵。

另外,如果列舉所有狀態會導致複雜度爆炸,因此,我們先預處理對於每一行而言合法的狀態(即十字兩格不能有友軍),這樣在資料範圍拉滿的情況下也就只有\(60\)種情況,這樣就足夠存下了。

#include <bits/stdc++.h>
#define ll long long
#define pb push_back
#define fast ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
using namespace std;
const int maxn = 110;
string mp[maxn];
int c[maxn];
ll f[maxn][70][70]={0};
vector<int> v;
int num[70]={0};
void init(int n)
{
    for (int i = 0; i < (1 << n); i++)
    {
        if (!((i << 1) & i) && !((i << 2) & i))
            v.pb(i);
    }
}
int main()
{
    int n, m;
    cin >> n >> m;
    init(m);
    for (int i = 0; i < n; i++)
    {
        cin >> mp[i];
    }
    for(int i=0;i<n;i++)
    {
        for(int j=0;j<m;j++)
        {
            if(mp[i][j]=='P') c[i+1]=(c[i+1]<<1)+0;
            else c[i+1]=(c[i+1]<<1)+1;
        }
    }
    int len = v.size();
    for (int i = 0; i < len; i++)
    {
        int tmp = v[i];
        while (tmp)
        {
            num[i] += tmp % 2;
            tmp /= 2;
        }
    }
    for (int s = 1; s <= n; s++)
    {
        for (int i = 0; i < len; i++)
        {
            for(int j=0;j<len;j++)
            {
                for(int k=0;k<len;k++)
                {
                    if((v[i]&v[j]) || (v[i]&v[k]) || (v[j]&v[k]) || (v[i]&c[s]) || (v[j]&c[s-1]) || (v[k]&c[max(0,s-2)])) continue;
                    f[s][i][j]=max(f[s][i][j],f[s-1][j][k]+num[i]);
                }
            }
        }
    }
    ll ans = 0;
    for (int i = 0; i < len; i++)
    {
        for (int j = 0; j < len; j++)
        {
            ans = max(ans, f[n][i][j]);
        }
    }
    cout << ans << '\n';
}

NC16122 郊區春遊

NC16544 簡單環

這兩道題寫在一起是因為這兩道題同屬於TSP問題。

TSP問題,又稱旅行推銷員問題,假設一個商人要拜訪\(n\)個城市,每個城市只能拜訪一次,並且最後回到原點,問所有路徑中的最小權值。

這類問題通常而言,用\(f[i][j]\)表示\(i\)狀態下並且當前處於\(j\)位置時的最小權值,\(i\)為\(01\)串,1表示已經拜訪,0表示未拜訪。

另外,狀態轉移方程不僅僅侷限於依靠過去推現在,也可以通過現在推將來。

例如NC16122,狀態轉移方程可以寫為:\(f[i|nex][k]=min(f[i|nex][k],f[i][j]+G[vi[j]][vi[k]])\),其中\(nex=1<<(k-1)\)。

另外對於這道題,先要進行連通性與最短路徑的預處理,\(floyd\)即可。

#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define fast ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
using namespace std;
const int maxn = 210;
const int INF = 0x3f3f3f3f;
int n,m,r;
int G[maxn][maxn];
int f[1<<16][maxn];
int size=10;
int vi[20];
void floyd()
{
    for(int k=1;k<=n;k++)
    {
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=n;j++)
            {
                G[i][j]=min(G[i][j],G[i][k]+G[k][j]);
            }
        }
    }
}
int main()
{
    fast;
    cin>>n>>m>>r;
    int u,v,w;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
        {
            G[i][j]=INF;
        }
    }
    for(int i=1;i<=r;i++)
    {
        cin>>vi[i];
    }
    for(int i=0;i<(1<<r);i++)
    {
        for(int j=1;j<=n;j++)
        {
            f[i][j]=INF;
        }
    }
    for(int i=1;i<=m;i++)
    {
        cin>>u>>v>>w;
        G[u][v]=w;
        G[v][u]=w;
    }
    floyd();
    for(int i=1;i<=r;i++)
    {
        f[1<<(i-1)][i]=0;
    }
    for(int i=1;i<(1<<r)-1;i++)
    {
        for(int j=1;j<=r;j++)
        {
            int sta=1<<(j-1);
            if(!(i&sta)) continue;
            for(int k=1;k<=r;k++)
            {
                int nex=1<<(k-1);
                if(nex&i) continue;
                f[i|nex][k]=min(f[i|nex][k],f[i][j]+G[vi[j]][vi[k]]);
            }
        }
    }
    int ans=INF;
    for(int i=1;i<r;i++)
    {
        ans=min(ans,f[(1<<r)-1][i]);
    }
    cout<<ans<<'\n';
}

那麼對於另一道題,首先要求這個環要求終點要大於起點(不然統計會亂),狀態轉移方程與上一題類似,區別僅僅在於統計的是方案數量。轉移之後,判斷當前點與起點是否有邊且環長度是否大於3。最後得到的結果需要除2(因為對於一個環可以順時針也可以逆時針),那麼這裡就需要逆元。

#include<bits/stdc++.h>
#define pb push_back
#define ppb pop_back
#define ll long long
#define fast ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
using namespace std;
const int maxn = 22;
const ll mod = 998244353;
bool G[maxn][maxn];
ll f[1<<20][maxn];
ll ans[maxn];
int n,m,k;
ll Pow(ll a,ll b)
{
    if(b==0) return 1;
    if(b%2) return a*Pow(a,b-1)%mod;
    ll tmp=Pow(a,b/2);
    return tmp*tmp%mod;
}
int main()
{
    fast;
    cin>>n>>m>>k;
    int u,v;
    for(int i=1;i<=m;i++)
    {
        cin>>u>>v;
        G[u][v]=1;
        G[v][u]=1;
    }
    for(int i=1;i<=n;i++)
    {
        f[1<<(i-1)][i]=1;
    }
    int len=(1<<n)-1;
    for(int i=1;i<=len;i++)
    {
        int s;
        for(int j=1;j<=n;j++)
        {
            if(i>>(j-1)&1)
            {
                s=j;
                break;
            }
        }
        for(int j=1;j<=n;j++)
        {
            int t=1<<(j-1);
            if(!(i&t)) continue;
            for(int k=s+1;k<=n;k++)
            {
                int kk=1<<(k-1);
                if(i&kk) continue;
                if(G[j][k])
                {
                    f[i|kk][k]=(f[i|kk][k]+f[i][j])%mod;
                }
            }
            if(G[j][s])
            {
                int len=__builtin_popcount(i);
                if(len>=3)
                {
                    ans[len%k]=(ans[len%k]+f[i][j])%mod;
                    // cout<<i<<' '<<j<<' '<<ans[0]<<'\n';
                }
            }
        }
    }
    int inv=Pow(2,mod-2);
    for(int i=0;i<k;i++)
    {
        cout<<ans[i]*inv%mod<<'\n';
    }
}

POJ2411 Mondriaan's Dream

棋盤覆蓋問題,僅能用\(1 \times 2\)或者\(2 \times 1\)的骨牌去填充\(h \times w\)的棋盤。

我們可以把每一行當前是否被覆蓋進行狀態壓縮,用\(1\)表示已覆蓋,\(0\)表示未覆蓋。那麼對於每一行而言,可以留出一定的\(0\)來給下一行覆蓋\(2 \times 1\)的機會。

那麼考慮狀態轉移的條件。首先必須保證之前的行全部被填滿,用\(j\)表示當前行狀態,用\(k\)表示上一行狀態,我們需要把上一行中\(0\)的位置填滿,不然之後無論如何都不能做到密鋪。其次,在當前行中排除\(2 \times 1\)的骨牌,剩下的\(1\)就是\(1 \times 2\)骨牌的位置,那麼就需要保證連續\(1\)的個數必須為偶數。這裡可以進行預處理,存下所有合法的狀態。

剩下就是狀態轉移。當上述條件全部滿足時,\(f[i][j]=\sum f[i-1][k]\),初始條件為\(f[0][len-1]=1\),其中\(len=(1<<w)\)。

#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define fast ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
using namespace std;
int state[1<<11];
ll f[13][1<<11];
void init()
{
    for(int i=0;i<(1<<11);i++)
    {
        state[i]=1;
        int j=i;
        int tmp=0;
        while(j)
        {
            if(j%2==1) tmp++;
            else
            {
                if(tmp%2)
                {
                    state[i]=0;
                    break;
                }
                tmp=0;
            }
            j/=2;
        }
        if(tmp%2) state[i]=0;
    }
}
int main()
{
    fast;
    int h,w;
    init();
    while(cin>>h>>w)
    {
        if(h==0 && w==0) break;
        int len=(1<<w);
        for(int i=0;i<=h;i++)
        {
            for(int j=0;j<len;j++)
            {
                f[i][j]=0;
            }
        }
        f[0][len-1]=1;
        // for(int i=0;i<len;i++)
        // {
        //     cout<<i<<' '<<state[i]<<'\n';
        // }
        for(int i=1;i<=h;i++)
        {
            for(int j=0;j<len;j++)
            {
                for(int k=0;k<len;k++)
                {
                    if(state[j&k] && (j|k)==len-1)
                    {
                        f[i][j]+=f[i-1][k];
                    }
                }
            }
        }
        cout<<f[h][len-1]<<'\n';
    }
}