P11844 [USACO25FEB] Friendship Editing G
题目描述
Farmer John 的 \(N\) 头奶牛编号为 \(1\) 到 \(N\)(\(2\le N\le 16\))。奶牛之间的朋友关系可以建模为一个有 \(M\)(\(0\le M\le N(N-1)/2\))条边的无向图。两头奶牛为朋友当且仅当图中她们之间存在一条边。
在一次操作中,你可以添加或删除图中的一条边。计算确保以下性质成立所需的最小操作次数:如果奶牛 \(a\) 和 \(b\) 是朋友,则对于每头其他奶牛 \(c\),\(a\) 和 \(b\) 中至少之一与 \(c\) 是朋友。
输入格式
输入的第一行包含 \(N\) 和 \(M\)。
以下 \(M\) 行,每行包含一对朋友 \(a\) 和 \(b\)(\(1\le a<b\le N\))。每对朋友出现至多一次。
输出格式
输出你需要增加或删除的边的数量。
输入输出样例 #1
输入 #1
3 1
1 2
输出 #1
1
输入输出样例 #2
输入 #2
3 2
1 2
2 3
输出 #2
0
输入输出样例 #3
输入 #3
4 4
1 2
1 3
1 4
2 3
输出 #3
1
说明/提示
样例 1 解释:
该网络不符合性质。我们可以添加边 \((2,3)\) 或 \((1,3)\),或删除边 \((1,2)\) 进行修复。
样例 2 解释:
不需要进行任何修改。
- 测试点 \(4\sim 13\):对于每一个 \(N\in [6, 15]\) 依次有一个测试点。
- 测试点 \(14\sim 18\):\(N=16\)。
解题报告
我艹,牛逼!!!
先给出一个很难发现的性质:每一个符合条件的关系图,这个图的补图是由若干不相交团组成(团是完全图)。
下面是一个经典的证明:
如果奶牛 a 和 b 是朋友,则对于每头其他奶牛 c,a 和 b 中至少之一与 c 是朋友,假设 a,b 不是朋友,就会有两种情况。(c 为 a、b 外任意一点)
1. a b 2. a b\ /c c
考虑建补图:
1. a---b 2. a---b\ /c c
将图扩展可以推出,a、b 外任意一个点要么和 a、b 都有连接,要么和 a、b 都没连接。同理,两个点与同一个点都有连接,那这两个点也有连接。
所以只要有 a、b 外的两点 c、d 都跟 a 和 b 有连接,c、d 之间就也会有连接,这时 a、b、c、d 就会形成一个团,所以整个图就是很多团的并集。
所以问题就转换成了求将一个图的补图修改成很多团的并集的最小操作次数。
根据数据范围的提示,我们可以尝试状压 DP。
设点集 \(i\) 表示被选择的点,状态数组为 \(dp[i]\) 表示将这些点改成若干团的并的最小操作次数。
显然有以下转移:
\(cost(j \oplus i)\)表示的是将点集 \(j \oplus i\) 代表的图改成团的最小代价。这个方程表示从点集 \(i\) 中的一些点改成完全图的最小代价加上剩余点改成若干个团的最小代价。
每个点集 \(i\) 的 \(cost(i)\) 都可以被预处理出来,统计每两个属于点集 \(i\) 的点之间不存在边的个数(连边),再加上每个属于点集 \(i\) 的点和每个不属于点集 \(i\) 的点之间存在的边的个数(删边),就完成了。
预处理部分时间复杂度 \(O(n^2 \times 2^n)\),状压 DP 部分时间复杂度 \(O(3^n)\),代码如下:
#include<bits/stdc++.h>
using namespace std;
const int INF=0x3f3f3f3f;
const int N=21;#define ckmax(x,y) ( x=max(x,y) )
#define ckmin(x,y) ( x=min(x,y) )inline int read()
{int f=1,x=0; char ch=getchar();while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }while(isdigit(ch)) { x=x*10+ch-'0'; ch=getchar(); }return f*x;
}bool e[N][N];
int n,m;
int g[1<<N];
int dp[1<<N];signed main()
{n=read(),m=read();for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)e[i][j]=1;for(int i=1;i<=m;i++){int u=read(),v=read();e[u][v]=e[v][u]=false;}for(int k=0;k<(1<<n);k++)for(int i=1;i<=n;i++){if( !( k&(1<<i-1) ) ) continue;for(int j=1;j<=n;j++){if(i==j) continue;g[k]+=( k&(1<<j-1) ? (!e[i][j]):e[i][j] );}}for(int i=0;i<(1<<n);i++){dp[i]=g[i];for(int j=i;j;j=(j-1)&i)ckmin(dp[i],dp[i^j]+g[j]);}printf("%d",dp[(1<<n)-1]/2);return 0;
}