简介:
有n个人,ta们并不是都认识对方
要把ta们恰好分成两组,使得每组之中的人都互相认识
分析:
人与人之间的关系就像一张图
显然,互相不认识的人(包括X不认识Y且Y不认识X,X认识Y但是Y不认识X,X不认识Y但是Y认识X这三种情况)一定不会在同一个组内,
因此我们可以根据认识关系建出一个图:不认识的人之间连一条边(X认识Y,Y不认识X的情况也要连接XY)
之后我们就可以二分图染色,把一个连通块染成两种颜色(B&W)
这样我们就形成了两个互相对立的集合
注意:如果我们二分图染色不成功,说明无解
由于会有多个连通块,对于每个连通块都(至多)有两个集合:B&W,我们有两种方案:
- B—>1组,W—>2组
- B—>2组,W—>1组
我们通过分配这些集合,使得两个组的人数相差最小
我们要怎么确定集合到底属于哪个组呢?
动态规划
有的资料上把这个问题转化成了01背包
但是在0-1背包中,目标很明确:装的物品总价值最大,因此每次决策都要从两种决策中选出使得物品总价值最大的
相比之下,这道题的动态规划,不应该从两种决策中选出“两队人数差最小的”,如果用这种贪心方法,会丢失全局最优解
事实上,只有最后一个连通块的决策才有着明确的目标,动态规划的其他中间过程在做决策时,是无法判断优劣的,只能枚举所有可能性
我们利用dp只是维护差值的可行性
说是0-1背包的变式还是有点牵强的
dfs复杂度O(n)(每个人恰好访问一次),动态规划复杂度O(n^2)(枚举决策复杂度将是O(2^n),这里用动态规划的思想消除了重叠自问题,很好的降低了复杂度),总复杂度O(n^2)
这道题的分析到此结束了,思路很清晰,但是实现有一定的难度,所以下面就针对代码详细解释一下:
主程序
int main()
{
int T,cas=0;
scanf("%d",&T);
while (T--)
{
if (cas++) printf("\n");
memset(G,1,sizeof(G)); //我们用G这个邻接矩阵存储图,比较方便
C.clear();
scanf("%d",&n);
for (int i=0;i<n;i++)
{
int x;
while (scanf("%d",&x)!=EOF&&x)
G[i][x-1]=0;
}
ff=1; //判断二分图染色是否成功的变量
doit();
if (ff) dp();
else printf("No solution\n");
}
return 0;
}
我们在主程序中看到了一个变量C,先不具体进行解释(随着程序的进行,我们会看到ta的用处的)
二分图染色
struct node{ //看一下,这就是C和cc的定义
vector<int> g[2]; //cc表示一个连通块,把一个连通块的两个集合打包成一个
void clear() //C记录的就是所有连通块(cc的一个集合)
{
g[0].clear();
g[1].clear();
}
};
node cc;
vector<node> C;
int dfs(int now,int flag)
{
a[now]=flag;
for (int i=0;i<n;i++){
if (i!=now&&(G[now][i]||G[i][now])){ //如果两个点之间有连边(注意判断方式)
if (a[i]==-1&&!dfs(i,flag^1)) return 0;
if (a[i]!=-1&&a[i]==a[now]) return 0;
}
}
if (flag==0) cc.g[0].push_back(now); //cc是一个vector,记录的就是当前连通块的染色状况
else cc.g[1].push_back(now);
return 1;
}
void doit()
{
memset(a,-1,sizeof(a)); //每个节点的颜色
for (int i=0;i<n;i++)
if (a[i]==-1) //还未染色
{
cc.clear(); //一个连通块,分成两部分
if (!dfs(i,0)) {ff=0; return;} //染色失败
else C.push_back(cc);
}
}
程序中大量使用vector,这样代码复杂度才不会太高
动态规划
这里有一点小细节:
首先我们把两种不同的方法抽象成两个数(B—>1组,W—>2组;B—>2组,W—>1组)
每个数代表的是:1组大小-2组大小(1组较大,数就为正;否则为负数)
dp的状态是:f[i][j]表示考虑前i个集合,状态j是否可以达到(我们只维护可行性)
这个状态j可正可负,实际上就是前面i个集合的【1组大小-2组大小(1组较大,数就为正;否则为负数)】之和
这就引出了一个问题:C++的数组下标不能为负
所以我们需要j下标都加上一个数,使ta变成非负数(加上n即可)
注意我们在记录状态的时候,两种不同的决策,f[i][j]的值是一正一负,这是为了方便解的打印
void dp()
{
memset(f,0,sizeof(f));
cnt=C.size(); //连通块的个数
for (int i=0;i<cnt;i++)
{
int d=C[i].g[0].size()-C[i].g[1].size(); //两个集合的size差
for (int j=0;j<=2*n;j++) //这里为什么是0~2n呢,实际上是-n~n,我们都加上了n,保证数组下标非负
{
if (i==0)
{
f[i][n+d]=1; //n--->0 两种方法:black-->group A white-->group B
f[i][n-d]=-1; // black-->group B white-->group A
}
else
{
if (f[i-1][j]){ //只维护可行性
f[i][j+d]=1;
f[i][j-d]=-1;
}
}
}
}
for (int i=0;i<=n;i++) //解的打印
{
if (f[cnt-1][n+i])
{
print(n+i);
return;
}
if (f[cnt-1][n+i])
{
print(n-i);
return;
}
}
}
解的打印
void insert(vector<int>& a,vector<int>& b) //解的复制
{
for (int i=0;i<a.size();i++) b.push_back(a[i]);
}
void print(int j)
{
vector<int> ans[2];
for (int i=cnt-1;i>=0;i--)
{
int d=C[i].g[0].size()-C[i].g[1].size();
if (f[i][j]==1) //值为1:方案1,颜色为0的在1组,颜色为1的在2组
{
insert(C[i].g[0],ans[0]);
insert(C[i].g[1],ans[1]);
j-=d;
}
else if (f[i][j]==-1) //值为-1:方案1,颜色为0的在2组,颜色为1的在1组
{
insert(C[i].g[0],ans[1]);
insert(C[i].g[1],ans[0]);
j+=d;
}
}
printf("%d",ans[0].size());
for (int i=0;i<ans[0].size();i++)
printf(" %d",ans[0][i]+1); //不要忘了+1,我们一开始为了节约空间,是从0开始编号的
printf("\n");
printf("%d",ans[1].size());
for (int i=0;i<ans[1].size();i++)
printf(" %d",ans[1][i]+1);
printf("\n");
}
tip
这道题很好的体现了stl的运用,熟练掌握一些必要的stl,对于代码实现是很有好处的
//这里写代码片
#include<cstdio>
#include<cstring>
#include<iostream>
#include<vector>
using namespace std;
const int INF=0x33333333;
bool G[103][103],ff;
int n,a[103],f[103][2*103],cnt;
struct node{
vector<int> g[2];
void clear()
{
g[0].clear();
g[1].clear();
}
};
node cc;
vector<node> C;
int dfs(int now,int flag)
{
a[now]=flag;
for (int i=0;i<n;i++){
if (i!=now&&(G[now][i]||G[i][now])){ //如果两个点之间有连边(注意判断方式)
if (a[i]==-1&&!dfs(i,flag^1)) return 0;
if (a[i]!=-1&&a[i]==a[now]) return 0;
}
}
if (flag==0) cc.g[0].push_back(now); //cc是一个vector,记录的就是当前连通块的染色状况
else cc.g[1].push_back(now);
return 1;
}
void doit()
{
memset(a,-1,sizeof(a)); //每个节点的颜色
for (int i=0;i<n;i++)
if (a[i]==-1) //还未染色
{
cc.clear(); //一个连通块,分成两部分
if (!dfs(i,0)) {ff=0; return;} //染色失败
else C.push_back(cc);
}
}
void insert(vector<int>& a,vector<int>& b)
{
for (int i=0;i<a.size();i++) b.push_back(a[i]);
}
void print(int j)
{
vector<int> ans[2];
for (int i=cnt-1;i>=0;i--)
{
int d=C[i].g[0].size()-C[i].g[1].size();
if (f[i][j]==1)
{
insert(C[i].g[0],ans[0]);
insert(C[i].g[1],ans[1]);
j-=d;
}
else if (f[i][j]==-1)
{
insert(C[i].g[0],ans[1]);
insert(C[i].g[1],ans[0]);
j+=d;
}
}
printf("%d",ans[0].size());
for (int i=0;i<ans[0].size();i++)
printf(" %d",ans[0][i]+1);
printf("\n");
printf("%d",ans[1].size());
for (int i=0;i<ans[1].size();i++)
printf(" %d",ans[1][i]+1);
printf("\n");
}
void dp()
{
memset(f,0,sizeof(f));
cnt=C.size();
for (int i=0;i<cnt;i++)
{
int d=C[i].g[0].size()-C[i].g[1].size(); //两个集合的size差
for (int j=0;j<=2*n;j++) //这里为什么是0~2n呢,实际上是-n~n
{
if (i==0)
{
f[i][n+d]=1; //n--->0 两种方法:black-->group A white-->group B
f[i][n-d]=-1; // black-->group B white-->group A
}
else
{
if (f[i-1][j]){
f[i][j+d]=1;
f[i][j-d]=-1;
}
}
}
}
for (int i=0;i<=n;i++)
{
if (f[cnt-1][n+i])
{
print(n+i);
return;
}
if (f[cnt-1][n+i])
{
print(n-i);
return;
}
}
}
int main()
{
int T,cas=0;
scanf("%d",&T);
while (T--)
{
if (cas++) printf("\n");
memset(G,1,sizeof(G));
C.clear();
scanf("%d",&n);
for (int i=0;i<n;i++)
{
int x;
while (scanf("%d",&x)!=EOF&&x)
G[i][x-1]=0;
}
ff=1;
doit();
if (ff) dp();
else printf("No solution\n");
}
return 0;
}