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