状压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);
    }
}

 

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