狀壓dp水題練習

狀壓dp水題練習

吉比特筆試第二題(狀壓dp)

題目大意:

給定一個長度爲n(n<=15)的數字,每一位爲1~9中的一個數字,現在你可以將整個數字進行全排列打亂,問你新組成的數字中有多少個數字是m的倍數?(m<=50)

比如,S=123,總共有6種排列:123,132,213,231,312,321,其中爲m=6的倍數有2個:132,312

sample input

123 6

sample output

2

思路:

全排列肯定不得行,15!卡死你,所以從狀壓dp入手。

dp(i,t)表示當前狀態爲i,且餘數爲t的方案數。

那就變成一個套路了:第一維枚舉當前選擇的狀態,第二維枚舉未被選擇的數j,將j插入在當前狀態的最後,進行轉移。

dp[i ^ (1 << j)][(t * 10 + (s[j] - '0')) % m] += dp[i][t];

不過這個題目中還有一個去重的陷阱,如果1出現了兩次或多次,那麼你就重複計算了,如何去重呢?

去重方法一:

這是我在牛客網上看到的方法:將數據排序,對於兩個相同的數A和B,什麼時候會重複計算呢?選了A不選B,或者選了B不選A,這兩種是重複計算的。所以我們在排序後加一個小判斷:

  1. j-1與j不相等,j-1對j沒有影響

  2. j-1與j相等,但是之前我已經選擇過了j-1,此時我再選j也沒有影響(就像選兩個4,組成44,也是一種新的組合)

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#include <cstdlib>
using namespace std;
typedef long long ll;
ll dp[1 << 16][55];
char s[50];
 
int main() {
    //freopen("E:\\test_data\\in.txt","r",stdin);
    //freopen("E:\\test_data\\out1.txt","w",stdout);
    ll l, r, n, m, i, j, t;
    while (scanf("%s %lld", s, &m) != EOF) {
        n = strlen(s);
        memset(dp, 0, sizeof(dp));
        sort(s, s + n);//對數字進行排序
        dp[0][0] = 1;
        for (i = 0; i < (1LL << n); i++) {
            for (j = 0; j < n; j++) {
                if (!(i & (1LL << j))) {
                    if (j == 0 || s[j] != s[j - 1] || (i & (1LL << (j - 1)))) {//保證對於同一數字按序
                        for (t = 0; t < m; t++) {
                            dp[i ^ (1LL << j)][(t * 10 + (s[j] - '0')) % m] += dp[i][t];
                        }
                    }
                }
            }
        }
        printf("%lld\n", dp[(1 << n) - 1][0]);
    }
}

去重方法二:

上面的方法有點難想,那麼考慮在最後結果去重。

如果在數字串中,2出現了2次,4出現了3次,那麼會有多少種重複呢?答案是(2!*3!)。所以我們直接dp方案,最後除以這個數就是答案。

#include <bits/stdc++.h>
#define int long long 
using namespace std;
​
const int maxn=1<<16;
int dp[maxn][50];
char s[20];
int cnt[20];
signed main(){
    while(scanf("%s",s)!=EOF){
        int n,m;
        scanf("%lld%lld",&n,&m);
        int len=strlen(s);
        memset(dp,0,sizeof(dp));
        memset(cnt,0,sizeof(cnt));
        for(int i=0;i<n;i++){
            cnt[s[i]-'0']++;
        }
        dp[0][0]=1;
        for(int i=0;i<(1<<n);i++){
            for(int j=0;j<n;j++){
                if(!(i&(1<<j))){
                    for(int t=0;t<m;t++){
                        dp[i|(1<<j)][(t*10+s[j]-'0')%m]+=dp[i][t];
                    }
                }
            }
        }
        int ans=dp[(1<<n)-1][0];
        for(int i=1;i<=9;i++){
            if(cnt[i]){
                int temp=1;
                for(int j=1;j<=cnt[i];j++){
                    temp*=j;
                }
                ans/=temp;
            }
        }
        printf("%lld\n",ans);
    }
    return 0;
}

洛谷P1879玉米田

題目大意:

農場主John新買了一塊長方形的新牧場,這塊牧場被劃分成M行N列(1 ≤ M ≤ 12; 1 ≤ N ≤ 12),每一格都是一塊正方形的土地。John打算在牧場上的某幾格裏種上美味的草,供他的奶牛們享用。

遺憾的是,有些土地相當貧瘠,不能用來種草。並且,奶牛們喜歡獨佔一塊草地的感覺,於是John不會選擇兩塊相鄰的土地,也就是說,沒有哪兩塊草地有公共邊。

John想知道,如果不考慮草地的總塊數,那麼,一共有多少種種植方案可供他選擇?(當然,把新牧場完全荒廢也是一種方案)

輸入格式

第一行:兩個整數M和N,用空格隔開。

第2到第M+1行:每行包含N個用空格隔開的整數,描述了每塊土地的狀態。第i+1行描述了第i行的土地,所有整數均爲0或1,是1的話,表示這塊土地足夠肥沃,0則表示這塊土地不適合種草。

輸出格式

一個整數,即牧場分配總方案數除以100,000,000的餘數。

思路:

對於每一行,只有12個列,也就是說所有行的狀態總數不超過2^12,所以,我們對每一行進行狀態壓縮,並且預處理每一種狀態的合法性:一個狀態合法,必須沒有連續的兩個1,並且對於每行,合法就另說了。

dp(s,j)表示在第j行,狀態爲s的方案數,從j推向j+1,可以再遍歷2^12的所有狀態,如果合法就累加,推導j+1行。

#include <iostream>
#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#define int long long 
using namespace std;
​
const int maxn=1<<12+10;
const int mod=100000000;
int dp[20][maxn];
int mp[maxn];
int mp1[maxn];
signed main(){
    int n,m;
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            int temp;
            scanf("%lld",&temp);
            mp[i]+=(temp<<(j-1));//狀態壓縮
        }
    }
    //判斷哪些狀態是合法的
    for(int i=0;i<(1<<m);i++){
        mp1[i]=!((i<<1)&i&&(i>>1)&i);//不能同時有兩個相鄰的1
    }
    dp[0][0]=1;
    for(int i=1;i<=n;i++){
        for(int j=0;j<(1<<m);j++){
            if(mp1[j]&&((mp[i]&j)==j)){
                for(int k=0;k<(1<<m);k++){
                    if(!(j&k)){
                        dp[i][j]+=dp[i-1][k];
                        dp[i][j]%=mod;
                    }
                }
            }
        }
    }
    int ans=0;
    for(int i=0;i<(1<<m);i++){
        ans+=dp[n][i];
        ans%=mod;
    }
    printf("%lld\n",ans);
    return 0;
}

 

ZOJ3471:Most Powerful

題目大意:

給定一個數組n(1<=n<=10),其中有n個數,如A1,A2....An。

再給一個能量數組mp(n,n)

任何兩個數相碰撞,將會釋放出mp(i,j)的能量,並且隨意留下i和j中的一個。

你的任務是安排這些數的碰撞順序,使得最後獲得的能量最大。

注意多組輸入,10!不可取。

sample input

2
0 4
1 0
3
0 20 1
12 0 1
1 10 0
0

sample output

4
22

思路:

很明顯又是狀壓解決全排列的經典模型。

dp(s)表示狀態s能夠獲得的最大能量。

則直接枚舉不在s中的元素j,與在s中的元素k任意碰撞,最後留下j的最大能量就ok(爲什麼留下j呢?其實留下誰都無所謂,最後肯定是所有狀態都搜索到。)

#include <bits/stdc++.h>
using namespace std;
​
const int maxn=1<<11;
​
int dp[maxn];
int mp[12][12];
void solve(int n){
    for(int i=0;i<n;i++){
        for(int j=0;j<n;j++){
            scanf("%d",&mp[i][j]);
        }
    }
    memset(dp,0,sizeof(dp));
    for(int i=0;i<(1<<n);i++){
        for(int j=0;j<n;j++){
            if(!(i&(1<<j))){
                for(int k=0;k<n;k++){
                    if(i&(1<<k)){
                        dp[i|(1<<j)]=max(dp[i|(1<<j)],dp[i]+mp[k][j]);
                    }
                }
            }
        }
    }
    printf("%d\n",dp[(1<<n)-1]);
}
signed main(){
    int n;
    while(scanf("%d",&n)!=EOF&&n){
        solve(n);
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章