经典的状态压缩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;
}