题目描述:
有n个物体和m个特征,每个物体用一个01串表示,第i位为1说明该特征该物体具有。我在心里想一个这n个物体的其中一个,你每次询问一个特征,我回答这个物体是否具备这个特征,当你确定答案后把答案告诉我。假设你采取最优策略,最少询问多少次就能保证猜到?(m<=11,n<=128)
思路:集合上的DP
因为每个特征不用问两遍,所以题目意思大致是问至少问多少个特征可以把所有物品区分开。决策就是问哪个特征,状态就是反馈回来的信息(在不在目标物体里)。
定义状态:d[s][a]:在所有问过的特征集合s中,集合a是目标里有的。很显然a是s的子集。
如何转移?假设现在要做决策,问 k 特征。那么有 两种情况:
1 目标里没有 k 这个特征
2 目标里有 k 这个特征
这个 k 其实把物品分成两堆,递归,取两堆当中需要问的次数最多的(保证能猜到)。这个结果不见得最优,所以需要取最小值。
容易想到,要问那些易于把物体区分开的特征,也就是如果所有物体都有k特征或都没有,这时候问k是没有意义的。所以,上面分成两堆的时候可以判断一下,如果任意一堆为空,这个决策都是无意义的,直接进行下一个。
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int INF = 0x3f3f3f3f;
const int maxn = 130;
const int maxm = 11;
int d[1<<maxm][1<<maxm], w[maxn];
int n, m;
// s: 已经问过的特征集合
// a: s中目标物体里包含的特征集合
int dp(int s, int a){
int& ans = d[s][a];
if(ans != INF) return ans;
int c = 0; // 统计满足条件的个数。条件:物体的特征集合 与 s 的交集 == a;
for(int i = 0; i < n; ++i) if((w[i] & s) == a) ++c;
// 1个物体就不需要问了,2个物体最少需要问一次。
if(c <= 1) return ans = 0;
if(c == 2) return ans = 1;
for(int f = 0; f < m; ++f){
if(s&(1<<f)) continue; // 如果 f 特征问过了
ans = min(ans, max(dp(s|(1<<f),a|(1<<f)), dp(s|(1<<f),a)) + 1);
}
return ans;
}
int main()
{
//freopen("in.txt","r",stdin);
while(scanf("%d %d",&m,&n) == 2&&n){
memset(d,INF,sizeof(d));
memset(w,0,sizeof(w));
char str[maxn];
for(int i = 0; i < n; ++i){
scanf("%s",str);
for(int j = 0; str[j]; ++j) if(str[j] == '1') w[i] |= (1<<j);
}
printf("%d\n",dp(0,0));
}
return 0;
}
优化。加上判断:
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 128;
const int maxm = 11;
int n,m, kase;
char objects[maxn][maxm];
int d[1<<maxm][1<<maxm], vis[1<<maxm][1<<maxm];
int cnt[1<<maxm][1<<maxm];
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 features = 0;
for(int f = 0; f < m; ++f)
if(objects[i][f] == '1') features |= (1<<f);
for(int s = 0; s < (1<<m); ++s)
++cnt[s][s & features];
}
}
int solve(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] == kase) return ans; // 省去了初始化vis数组,节省时间
vis[s][a] = kase;
ans = m;
for(int f = 0; f < m; ++f){
if(!(s & (1<<f))){ // f 特征还没有猜过
int s2 = s|(1<<f);
int a2 = a|(1<<f);
if(cnt[s2][a2] >= 1 && cnt[s2][a] >= 1){
int need = max(solve(s2, a2), solve(s2, a)) + 1;
ans = min(ans, need);
}
}
}
return ans;
}
int main()
{
freopen("in.txt","r",stdin);
kase = 0;
while(scanf("%d %d",&m,&n) == 2&&n){
++kase;
for(int i = 0; i < n; ++i) scanf("%s",objects[i]);
init();
printf("%d\n",solve(0,0));
}
return 0;
}