前言
写了我好久……实现也比较丑陋……连续9行清空…输出也比较恶心
题目
传送门:
Vjudge
UVa(有点慢)
题目大意:
有n(n<=100)个人,把他们分成非空的两组,使得每个人都被分到一组,且在同一组中的人互相认识,要求两组成员人数尽量接近,多解时输出任意一组方案,无解时输出No Solution.
输入时首先输入测试组数T,然后输入n,接下来n行第i行是对每个人i来说所认识的人的描述(读入0结束)
输出两行,每行首先输出每组人数,然后接着输出每组人的编号
in
i
n
2
5
3 4 5 0
1 3 5 0
2 1 4 5 0
2 3 5 0
1 2 3 4 0
5
2 3 5 0
1 4 5 3 0
1 2 5 0
1 2 3 0
4 3 2 1 0
out o u t
No Solution
3 1 3 5
2 2 4
思路
这道题我们发现两个人即使互相认识也不一定分到同一组,而两个人不认识(包括a认识b,b不认识a),也就是说两人互相不认识对于答案约束力比两人认识大,所以我们可以把两人互相不认识的关系看做一个图,我们可以得到一个或多个连通分量,如样例中的第二组数据,画出来的图如下:
我们可以发现,相邻的点必然不会分到同一组,距离为2的点必然在同一组,我们可以用类似于二分图的方法来黑白染色,我们假定连通分量中编号最小点颜色为白色:
那么如果出现环,并且返祖边的两个端点为同色,那么就No Solution,如样例一:
好了我们假设在单个连通分量中黑点有x个,白点有y个,那么我们就是把黑点给1组或者把y点给1组,而我们的目标就是1组和2组人数差最小,那我们令X为1组人数,Y为2组人数,我们要的是
min{abs(X−Y)}
m
i
n
{
a
b
s
(
X
−
Y
)
}
我们就可以想到这是一个0-1背包,我们把每个连通分量看作一件物品,
而这里单个物品的重量就是
abs(x−y)
a
b
s
(
x
−
y
)
,我们针对背包重量可以加上
abs(x−y)
a
b
s
(
x
−
y
)
也可以减去,我们的目标就是要让整个背包重量接近0,并且必须都放.
输出答案时我们用两种Vector,一种用来存储白点的人员编号,另一重存储黑点人员编号,Dp存储路径完后完后方便输出答案了
这道题主体思路就是这样,但实际上处理输出方案还是非常非常恶心的…
细节提示
1.这里0-1背包由于可加可减,所以我们要平移数组,我们平移输入的
n
n
的长度,那我们最后在找合法的且与最近n的
f[n][i]
f
[
n
]
[
i
]
就可以了
2.这里要保存路径,Dp完后找答案,所以不能开一维….
3.在DFS,找连通块连通块时注意不是遇到返祖边(回边)就无解,只有颜色不同时才无解
4.对于记录人员分组,我是用Vector记录的,这样Dp完后存储路径比较方便.
代码
#include<set>
#include<map>
#include<ctime>
#include<queue>
#include<cmath>
#include<cstdio>
#include<vector>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int read(){
int f=1,x=0;char s=getchar();
while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
return x*f;
}
#define MAXN 100
#define INF 0x3f3f3f3f
bool Map[MAXN+5],g[MAXN+5][MAXN+5];
vector<int> G[MAXN+5],Ans,S1[MAXN+5],S2[MAXN+5];
int w[MAXN+5],f[MAXN+5][2*MAXN+5],fa[MAXN+5][2*MAXN+5],vis[MAXN+5];
int DFS(int u,int f,int fa,int t){
vis[u]=1+f;//u当前节点,f:黑白点标记,fa:父亲,t:第t件物品(方案用)
if(!f) S1[t].push_back(u);//黑白点分别存储方案
else S2[t].push_back(u);
int siz=G[u].size(),ret=0,flag=0;
for(int i=0;i<siz;i++){
int v=G[u][i];//有回边但合法
if(v==fa||(vis[v]&&vis[v]!=vis[u])) continue;
else if(vis[v]==vis[u]) return INF;//不合法返回特殊值无解
int tmp=DFS(v,(f+1)%2,u,t);
if(tmp>100||tmp<-100){//儿子有特殊值无解返回
flag=1;
break;
}//统计黑白节点数量差
ret+=tmp;
}
return flag==1?INF:(ret+(!f?1:-1));
}
int pr[MAXN+5];
int main(){
int T=read();
while(T--){
bool flag=0;//len:物品个数
int n=read(),len=0;
Ans.clear();//初始化
memset(f,0,sizeof(f));
memset(g,0,sizeof(g));
memset(G,0,sizeof(G));//G[i]:与i相连的点
memset(pr,0,sizeof(pr));
memset(fa,0,sizeof(fa));
memset(S1,0,sizeof(S1));
memset(S2,0,sizeof(S2));
memset(vis,0,sizeof(vis));
for(int i=1;i<=n;i++){
int x;
while(1){
x=read();
if(!x) break;
Map[x]=1;
}//Map[x]:当前人i认识x(0,1表示)
for(int j=1;j<=n;j++)//g[i][j]:i,j两个人是否已连边
if(!Map[j]&&j!=i&&!g[i][j]&&!g[j][i]){//(0,1表示)
G[i].push_back(j);
G[j].push_back(i);
g[i][j]=g[j][i]=1;
}
memset(Map,0,sizeof(Map));
}
for(int i=1;i<=n;i++)
if(!vis[i]){//vis[i]:当前点所在连通分量是否访问过
++len,w[len]=abs(DFS(i,0,0,len));//可以不要绝对值
if(w[len]==INF){//无解
flag=1;
break;
}
}
if(flag){//无解
puts("No solution");
if(T) puts("");
continue;
}//fa[i][j]:此时f[i][j]是由f[i-1][fa[i][j]]转移过来的
f[0][n]=1;//f[i][j]:能否用前i个物品凑成重量为abs(j-n)
for(int i=1;i<=len;i++){//重量:两组人数差值
for(int j=0;j<=2*n;j++){//平移数组
if(j>=w[i]&&f[i-1][j-w[i]])//分两种放法(放1组或2组)
f[i][j]|=1,fa[i][j]=j-w[i];//记录路径
if(j+w[i]<=2*n&&f[i-1][j+w[i]])
f[i][j]|=1,fa[i][j]=j+w[i];
}
}
int x=len,y;//找最优分配方案
for(int i=0;i<=n;i++){
if(f[len][n-i]){
y=n-i;
break;
}
if(f[len][n+i]){
y=n+i;
break;
}
}
while(x){//寻找路径
int ny=fa[x][y];
int siz1=S1[x].size(),siz2=S2[x].size();
if(y<ny){//此处有些繁琐
if(siz1>siz2)
for(int i=0;i<siz2;i++)
Ans.push_back(S2[x][i]);
else for(int i=0;i<siz1;i++)
Ans.push_back(S1[x][i]);
}
else{
if(siz1<siz2)
for(int i=0;i<siz2;i++)
Ans.push_back(S2[x][i]);
else for(int i=0;i<siz1;i++)
Ans.push_back(S1[x][i]);
}
x--,y=ny;
}
printf("%d",Ans.size());
for(int i=0;i<int(Ans.size());i++)//标记已选的人编号
printf(" %d",Ans[i]),pr[Ans[i]]=1;
printf("\n%d",n-Ans.size());
for(int i=1;i<=n;i++)
if(!pr[i])//找未选的人
printf(" %d",i);
puts("");
if(T) puts("");//恶心的输出格式
}
return 0;
}
题外话
我写完代码发现其实物品的重量可以为负,那么输出方案时两个Vector的比较应该方便一些.