先看一道简单题 wyh2000 and pupil
题目网址:
http://acm.hdu.edu.cn/showproblem.php?pid=5285,
https://vjudge.net/problem/HDU-5285
思路
二分图染色判定能否满足题设条件,并在统计答案时使第一个集合尽量大
满足二分图的性质,对于认识的小学生之间可以任意合并
代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
using namespace std;
int n,m,ru,rv,tot,zero,one,t,ans1,ans2;
int first[200010],nxt[200010],color[200010];
bool flag;
struct edge
{
int u,v;
}l[200010];
queue<int>q;
void build(int f,int t)
{
l[++tot]=(edge){f,t};
nxt[tot]=first[f];
first[f]=tot;
}
int bfs(int s)
{
q.push(s);
color[s]=1;
while(!q.empty())
{
int k=q.front();
q.pop();
for(int i=first[k];i!=-1;i=nxt[i])
{
int x=l[i].v;
if(color[x]==-1)
{
q.push(x);
color[x]=color[k]^1;//color[x]=!color[k],color[x]=3-color[k];
}
if(color[x]==color[k])
return 0;
}
}
return 1;
}
void dfs(int s)
{
for(int i=first[s];i!=-1;i=nxt[i])
{
int x=l[i].v;
if(color[x]==color[s])
flag=1;
if(color[x]!=-1)
continue;
if(color[x]==-1)
{
color[x]=color[s]^1;
if(color[x]==1)
one++;
else zero++;
}
dfs(x);
}
}
int main()
{
scanf("%d",&t);
for(int i=1;i<=t;i++)
{
tot=0;
memset(first,-1,sizeof(first));//初始化
memset(color,-1,sizeof(color));
scanf("%d%d",&n,&m);
if(n<=1)
{
printf("Poor wyh\n");//不满足每组中至少有一个人的条件
continue;
}
if(m==0)
{
printf("%d 1\n",n-1);//由于wyh2000的意志,将一个小学生踢出集体2333
continue;
}
for(int i=1;i<=m;i++)
{
scanf("%d%d",&ru,&rv);
build(ru,rv);
build(rv,ru);
}
ans1=ans2=flag=0;
for(int i=1;i<=n;i++)
{
if(color[i]==-1&&first[i]!=-1)
{
one=1;
zero=0;
color[i]=1;
dfs(i);
ans1+=max(one,zero);//为了使第一组数量尽量多,将每次染色得出的较多的一组的数量加入第一组
ans2+=min(one,zero);//多次染色结果不会相互影响
}
if(color[i]==-1&&first[i]==-1)
ans1++;//只能染自己的情况,原理同上
}
if(flag==1)
printf("Poor wyh\n");
else printf("%d %d\n",ans1,ans2);
}
return 0;
}
/*
为什么要用二分图染色?
题目要求分为两个集合且元素之间对答案有价值的关系只存在于集合之间,而同一集合内的关系可以忽略
将元素看作点,关系转换为边即是二分图的模型
通过染色过程判断能否构成二分图即能否得到满足题设条件的分组情况,使所有不认识的关系,
只存在于集合之间,并将尽量多的相互认识的小学生归为于同一集合内
只需遍历一遍图,时间复杂度也没有问题 O(n)
也可以用并查集做,这里不再赘述
*/
再看一道扩展应用题 NOIP2010关押罪犯
题目网址:
http://codevs.cn/problem/1069/
思路:
二分+二分图染色
二分z市长看到的最小值,染色判断是否可行
对于边权小于等于(写等于与二分时统计答案方法有关)二分值可以直接忽略的证明,我的思路是考虑算法完成后,从最终划分结果方面来解读。而听闻XP同学的思路则是考虑程序的初始状态——所以边都在同一集合内,每次根据二分值将之分为两个满足条件的集合。两者理解思路本质上来说是相同的。也可以二分边界(<)处理。
链接
感谢wzhd同学沟通了我二分图染色和并查集的思路——两者殊途同归,并对讲题方法提出了有价值的建议 http://my.youkuaiyun.com/qq_36519085
感谢lxt同学帮忙找到了题目提交地址 http://www.studyai.com/emmmm
感谢XP学弟提供了新的证明思路 http://www.cnblogs.com/Loi-dfkdsmbd/
感谢RC同学提供了解题新思路 _ 多个树结构!? http://blog.youkuaiyun.com/lwlcagei
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
int n,m,ru,rv,rw,tot,s,ans;
int color[50010],first[500010],nxt[500010],w[100010];
bool flag;
struct edge
{
int u,v,w;
}l[1000010];
void build(int f,int t,int c)
{
l[++tot]=(edge){f,t,c};
nxt[tot]=first[f];
first[f]=tot;
}
int dfs(int k)
{
for(int i=first[k];i!=-1;i=nxt[i])
{
if(l[i].w<=s)//若当前找到的边的边权小于二分的最小值
continue; //即当前边对答案无影响-----若两端点在同一集合(监狱)内,此边权
int x=l[i].v;// 小于等于两集合内部最大值(此时二分的最小值)
if(color[x]==color[k])// -----若两端点位于不同集合,此边权
return 0;// 不会产生影响力___可行性
if(color[x]==-1)//且若用当前边进行染色可能将原本理应分在一个集合内的两点分到不同集合
{ //导致后来的染色过程发生冲突使答案偏大
color[x]=color[k]^1;//不能用来确定染色关系___必要性
if(!dfs(x))//这一部分不太理解的话可以先看下面
return 0;
}
}
return 1;
}
int check()
{
for(int i=1;i<=n;i++)
{
if(color[i]==-1&&first[i]!=-1)
{
color[i]=1;
if(!dfs(i))
return 0;
}
}
return 1;
}
int main()
{
memset(first,-1,sizeof(first));
memset(color,-1,sizeof(color));
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&ru,&rv,&rw);
w[i]=rw;//记录怨气值=可能造成的冲突影响力
build(ru,rv,rw);
build(rv,ru,rw);
}
sort(w+1,w+m+1);//从小到大排序-就可以二分啦...当然也可以从大到小.
int l=1,r=m,mid;
while(l+1<=r)//二分z市长看到的最小值-----对于同一集合内元素的怨气最大值,冲突影响力最大值
{
mid=(l+r)>>1;//二分边的编号代表最小值,而不是直接二分边权
s=w[mid]; //可以让我们取得最准确的限定条件
memset(color,-1,sizeof(color));
if(check())//染色判断是否可行
{
ans=w[mid];//统计可行答案
r=mid;
}
else l=mid+1;
}
if(l==1&&r<l)//若无轮多小的冲突影响力都不会出现,即本年内监狱中未发生任何冲突事件
printf("0");//其实没有必要写..因为这种情况下ans不会被更新值为0.
else printf("%d",ans);
return 0;
}
/*
为什么要用二分图染色?
题目要求分为两个集合,但对答案有影响的关系即存在于给定集合(也就是监狱)内部,也存在与集合之间
则可以自行限定另外的两个集合-----将对答案没有影响的关系(即边权小于等于二分最小值的边)
放于这两个集合内部,这些关系可以忽略,则其他对答案有影响的关系就位于集合之间,可以通过这些边染色
此时我们就得到了形如poor wyh题中的状况(二分图模型),只不过这个状况不是直接确定的,
而是由当前二分值确定的,于是,进行二分图染色判断当前状况是否合理即能否进行这样的分配,
最后二分出最小满足题目条件的解
*/
思路
并查集,比较难想
按怒气值从大到小排序,依次考虑每一条边
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
int n,m,ru,rv,rw;
int fa[500010],en[500010];
bool flag;
struct edge
{
int u,v,w;
}l[1000010];
bool cmp(edge a,edge b)
{
return a.w>b.w;
}
int find(int x)
{
return fa[x]==x?x:fa[x]=find(fa[x]);
}
void merge(int x,int y)
{
fa[find(y)]=find(x);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&ru,&rv,&rw);
l[i]=(edge){ru,rv,rw};
}
sort(l+1,l+m+1,cmp);//按边权从小到大排序
for(int i=1;i<=n;i++)
fa[i]=i;
for(int i=1;i<=m;i++)
{
if(find(l[i].u)==find(l[i].v))//若一条边的两端点已经位于一个集合中,则这条边的长度即为满足条件的最优解
{
printf("%d",l[i].w);
flag=1;
break;
}
if(!en[l[i].u])//若起点没有设置敌人,即由此点出发的边第一次被找到,则将终点设置为敌人
en[l[i].u]=l[i].v;
else merge(en[l[i].u],l[i].v);//否则将起点的敌人与终点放入同一集合内,作为可能的答案
if(!en[l[i].v])//单向建边,双向判断
en[l[i].v]=l[i].u;
else merge(en[l[i].v],l[i].u);
}
if(!flag)
printf("0");
return 0;
}
后记
实话说这里是演说中没有讲完的东西
在我先前写的版本中,代码注释及思路讲解都很少,可今天10.20为了突如其来的讲题,仔细研究了一下之前的代码,然后写了这些,也发现了许多之前理解的偏差之处。所以无论是讲题还是考试的失误原因,许多时候并非是因为没有掌握算法,而是理解不够深入造成的。
每做过一道题便将其理解透彻,我想这也是接下来的停课时间里,我们需要时刻谨记的东西吧。
2017.10.20 Loi_MurasameKatana