链接: https://vjudge.net/problem/UVA-11825
题意: 一个黑客入侵有n台计算机的网络,一共有n种服务,每台计算机运行着所有的服务。对于每一台计算机你都可以选择任何一项服务使得这台计算机以及其链接的计算机停止运行该项服务,假如有一项服务没有被任何计算机运行,那么则称:该服务瘫痪。
求: 服务瘫痪个数的最大值
一开始觉得计算机链接应该是双向的吧(事实证明好像不是) ,就以为可以用并查集来做,然后就WA了
其实一看到数据量就知道大概就是状态压缩dp了,题目有个很明显的特征就是N很小,1<=N<=16,反正按我目前这一点点做题量来说N小于20的一般就是状态压缩dp
建议不知道为什么可以用for来遍历状态的看一下我的另外一篇讲了状态压缩dp的文章:https://blog.csdn.net/weixin_43191865/article/details/88919246
既然知道是状态压缩,那么就要知道如何转移
服务瘫痪的条件就是没有任何一台计算机运行,因此只要我们找到任意一个计算机的集合覆盖全部计算机的,那么瘫痪数就+1
那么如何表示集合呢
首先我们引入一个num数组,使用二进制表示的状态作为num数组的值,num[i]表示第i台计算机所组成的集合。
比如num[1]=111 (二进制) 就表示第1台电脑连着第0,1,2台电脑(自己也连自己)
然后我们还需要表示集合覆盖呀,因此我们引入cover数组,表示 若干电脑链接而成形成的覆盖网络 。其下标为二进制状态,比如101,则是第0台电脑和第2台电脑组成的覆盖网络,同样用二进制表示覆盖网络作为其值。
最后引入dp数组,还是以若干电脑组成的覆盖网络 作为其下标,其值就表示该状态下服务瘫痪的最大个数。
那么我们可以得出转移方程:
dp[s]=max(dp[s],dp[ sub_s ^ s ]+1) sub_s 表示的是s的子状态
意思是: 假如sub_s 能覆盖完全部网络的话,那么他们就全部选择瘫痪同一个服务,并且瘫痪数+1
代码:(我后面会解释为什么可以那样遍历子状态)
#include<bits/stdc++.h>
using namespace std;
const int maxn=1<<17;
int num[17],cover[maxn],dp[maxn];
int main(){
int n,m,cnt=0;
while(scanf("%d",&n),n){
cnt++;
memset(num,0,sizeof(num));
memset(dp,0,sizeof(dp));
memset(cover,0,sizeof(cover));
for(int i=0;i<n;i++){
scanf("%d",&m);
while(m--){
int aim; scanf("%d",&aim);
num[i]|=(1<<aim); //aim从0开始
}
num[i]|=(1<<i); //漏了自己啊啊啊啊啊啊啊
}
int end=(1<<n);
for(int s=0;s<end;s++){ // 求所有覆盖
int i=0;
while(i<n){
if((s>>i) & 1 != 0) cover[s]|=num[i];
i++;
}
}
int flag=(1<<n)-1;
for(int s=0;s<end;s++){ /
for(int sub_s=s;sub_s;sub_s=(sub_s-1)&s){ // 如此遍历s的子集 为什么可以这样遍历
if(cover[sub_s]==flag) dp[s]=max(dp[s],dp[sub_s^s]+1);
}
}
cout<<"Case "<<cnt<<": "<<dp[flag]<<endl;
}
return 0;
}
为什么这样遍历子集的方法是正确的呢?
我们不如把问题换成:沿着 (sub_s-1)&s 方向递推为什么不会造成子集的缺漏
用二进制的角度看sub_s,首先s有的 1 , sub_s都有,在sub_s -1的时候,最多只能让一个1变为0,不会一次性让多个1变为0,因此不会造成子集缺漏。
(讲的不好,见谅。。。。)