1252 - Twenty Questions(状态压缩DP)

经典的状态压缩DP 。  有没有感觉这道题和什么东西有点像?  没错,是01背包 。 将特征看作物品 , 只不过这里的状态有点复杂, 需要用一个集合才能表示它, 所以我们用d[s][a]来表示,已经询问了特征集s , 假设我们要猜的物品是w ,w所具备的特征集为a ,此时还要询问的最小次数 。   显然a是s的子集,而且要注意本题的要求, 求的是最小化的最大询问次数 。也就是说无论猜哪个物品,猜这么多次一定能猜到 。  

那么状态如何转移呢? 就像背包问题,对于一个特征k ,我们要抉择:要k还是不要k ,因为并不是每个特征都是最优解中的 。     当然,为了求最大询问次数的那个物品,我们要递归找最大的,然而最优解就是所有k里最小的!   是不是有点晕 ? 还记得动归的特点吗? 大状态和小状态的定义以及表示都是一样的,而且大状态依赖于小状态的解 ,那么本题也是一样,如果实在不好理解,我们可以假设特征只有2个,来看看状态是如何转移的首先找最难找的物品,对于一个固定的k,要还是不要,取其中最大的,实际上已经是在找而且找到了这个最糟糕情况,那么我们又渴望选择最少的次数,那么选哪个k这个选择权在我们手中 ,所以我们要选择最小值(这些k都是可行解) 。  

还记得《最大面积最小的三角剖分》吗?  其递归思想有异曲同工之妙,只不过该题更难理解 。

另外,将各个状态具备a的所有特征的物品个数事先记录下来,将大大降低复杂度,因为在递归过程中,任何多余的操作都将导致时间复杂度极大的上升。

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int maxn_m = 12;
const int maxn_n = 130;
int m,n,d[1<<maxn_m][1<<maxn_m],vis[1<<maxn_m][1<<maxn_m],id=0,cnt[1<<maxn_m][1<<maxn_m];
char b[maxn_n][maxn_m+100];
void init() {
    for(int s=0;s<(1<<m);s++) {
        for(int a=s;a;a=(a-1)&s)
            cnt[s][a] = 0;
        cnt[s][0] = 0;
    }
    for(int i=0;i<n;i++) {
        int v = 0;
        for(int j=0;j<m;j++) if(b[i][j] == '1')
            v |= (1<<j);
        for(int s=0;s<(1<<m);s++)
            cnt[s][s&v] ++;
    }
}
int dp(int s,int a) {
    if(cnt[s][a] <= 1) return 0;
    if(cnt[s][a] == 2) return 1;
    int& ans = d[s][a];
    if(vis[s][a] == id) return ans;
    vis[s][a] = id;
    ans = m;
    for(int k=0;k<m;k++) {
        if(!(s&(1<<k))) {
            int s2 = s | (1<<k);
            int a2 = a | (1<<k);
            if(cnt[s2][a2]>=1&&cnt[s2][a]>=1) {
                int v = max(dp(s2,a2),dp(s2,a)) + 1;
                ans = min(ans,v);
            }
        }
    }
    return ans;
}
int main() {
    while(~scanf("%d%d",&m,&n)) {
        if(!m && !n) return 0;
        ++id;
        for(int i=0;i<n;i++) scanf("%s",b[i]);
        init() ;
        printf("%d\n",dp(0,0));
    }
    return 0;
}


你可能感兴趣的:(ACM,uva)