ABC398E题解
二分图?
怕有些同学不了解,所以还是打算简单说下:
(明白的可以先读一下题)
二分图,又称二部图,英文名叫 Bipartite graph。
二分图是什么?节点由两个集合组成,且两个集合内部没有边的图。
换言之,存在一种方案,将节点划分成满足以上性质的两个集合。
二分图的性质:
-
如果两个集合中的点分别染成黑色和白色,可以发现二分图中的每一条边都一定是连接一个黑色点和一个白色点。
-
二分图不存在长度为奇数的环
一个比较典型的例子,可以参考理解:
这里
题目大意
给定你一个树G,N个顶点编号由 \(1\) 到 \(n\) ,第i条边连接 \(U_i\) 和 \(V_i\) ,给定的树是联通无向无环图。
现在你和高桥对这个树依次进行操作,现在操作的玩家要找出一对整数 \((i,j)\) 且满足以下条件:
-
\(1≤i<j≤N\)
(如果高桥输出-1,-1代表他投降) -
G中的顶点\(i\),\(j\)没有边相连(换句话说,G中不存在\((i,j)\))
-
添加这条边后,G不会形成“奇循环”
当且仅当满足以下所有条件时, \(G\) 的顶点序列 \((v_0,v_1,\ldots,v_k)\) 称为奇循环:
- \(k\) 为奇数。
- \(v_0=v_k\) 。
- 对于每个 \(1\leq i \leq k\) ,有一条边连接 \(v_{i-1}\) 和 \(v_{i}\) 。
否则该玩家输掉游戏。
把题目说得简单一些,就是要在树G中添加若干条不存在的边,使得添加后不存在奇循环;这个题的理解难点也就在这里。
一个有思维含量的的想法:合法操作就是在添加边后G始终为二分图。
思路
首先不要被这个交互题唬住了,其实就是你和评测机对弈。不要忘了换行和return 0
我们的初步目的很显然了,尽可能找到一个使自己有利的局面,并把对面给逼死。
来思考一下可以添加的最大边数数量是多少。
以下是推论以及证明:
对于一棵有N个顶点的树,其合法的最大的边数为 \(N-1\)。
现在由题目,我们把它看为一个二分图。
在一个二分图中,如果顶点被划分为两个集合A和B,大小为\(a\)和\(b\),即\((a + b = N)\),则最大边数为 \(a*b\)。对于树,它是连通的二分图,因此可以明确划分为两个集合(例如通过二染色,上面引子已经提到)。
鉴于初始的图已经是一个二分图,将顶点分为集合A、B之后:
可以添加的所有边是连接A和B的所有可能边,减去已经存在的树边。
因此,得证:可以添加的最大边数是:
\(|A| * |B| - (N - 1)\)。
得到思路后就很明确了,计算完可以添加的最大边数后:
-
如果是奇数,选先手即可获胜。
-
否则,选后手即可。
那么我们现在要思考的重点就是如何找到可以添加的最大边数以及怎么实现。
代码实现
声明:
这个代码是从洛谷题解区里面扒过来加上注释了的
由于我的想法和这篇代码基本上是一致的,再加上这篇代码整体的风格和思路都比我的清晰
so就用这篇代码来讲实现))
邻接表建树,并使用DFS对这个二分图进行二染色,即划分二分图。
初始化:
const int N = 105; // 定义最大顶点数
int n, fa[N]; // n:顶点数;fa数组记录每个节点的父节点
vector<int> e[N], v1, v2; // e:邻接表;v1/v2:二分图的两个颜色集合
set<pair<int,int>> s; // 存储所有合法的可添加边(自动排序并去重)
二染色过程:
// 对树进行二染色,并划分顶点到v1和v2
inline void dfs(int u, int color)
{
color ^= 1; // 颜色翻转(1变0,0变1)
if (color)
v1.push_back(u); // 当前颜色为1时加入v1
else
v2.push_back(u); // 当前颜色为0时加入v2
// 遍历所有邻居节点
for (auto v : e[u])
{
if (v == fa[u]) continue; // 跳过父节点(避免回溯)
fa[v] = u; // 记录v的父节点为u
dfs(v, color); // 递归染色子节点
}
}
到这里,我们函数的判断逻辑就写完了。
接下来看主函数对过程的模拟:
初始部分:
cin >> n; // 输入顶点数n
// 输入树的n-1条边,构建邻接表e
int u, v;
for (int i = 1; i < n; i++)
{
cin >> u >> v;
e[u].pb(v);
e[v].pb(u); // 无向图,双向添加
}
// 从根节点1开始DFS染色,初始颜色为1,详情见上
dfs(1, 1);
接下来生成所有可能的边,并排除原有的:
// 生成所有可能的跨色边(v1和v2之间的边)
for (auto x1 : v1) //遍历集合v1
{
for (auto x2 : v2) //遍历集合v2
{
// 将边按字典序(小,大)插入集合s
s.insert(mk(min(x1, x2), max(x1, x2)));
}
}
// 移除树中已经存在的边(避免重复添加)
for (int i = 1; i <= n; i++)
{
if (fa[i]) // 如果i有父节点
{
// 将树边(i, fa[i])从s中移除
s.erase(mk(min(i, fa[i]), max(i, fa[i])));
}
}
如果前面都听懂了,后面就没什么技术含量了。按照题意一步步模拟对弈过程即可。
看代码吧。
// 若可添加边数为奇数,先手胜;否则后手胜
int x1, x2;
if (((int)(v1.size() * v2.size()) - n + 1) & 1)
{
cout << "First\n" << flush; // 先手
}
else
{
cout << "Second\n" << flush; // 后手
// 读取对手的移动并回应
cin >> x1 >> x2;
if (x1 == -1 && x2 == -1) return 0; // 游戏结束
// 移除对手选择的边
s.erase(make_pair(min(x1, x2), max(x1, x2)));
// 选择s中字典序最小的边
while (1)
{
pair<int,int> p = *s.begin(); // 获取最小边
cout << p.first << " " << p.second << "\n" << flush;
s.erase(p); // 移除已选边
// 读取对手下一步
cin >> x1 >> x2;
if (x1 == -1 && x2 == -1) break; // 游戏结束
// 移除对手新选的边
s.erase(mk(min(x1, x2), max(x1, x2)));
}
}
终版
#include<bits/stdc++.h>
#define pii pair<int,int>
#define pb push_back
#define mk make_pair
#define int long long
using namespace std;
const int N=105;
int n,fa[N];
vector<int>e[N],v1,v2;
set<pii>s;
inline void dfs(int u,int color)
{
color^=1;
if(color)v1.pb(u);
else v2.pb(u);
for(auto v:e[u])
{
if(v==fa[u])continue;
fa[v]=u;
dfs(v,color);
}
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
//freopen("filename.in","r",stdin);
//freopen("filename.out","w",stdout);
cin>>n;
int u,v;
for(int i=1;i<n;i++)
{
cin>>u>>v;
e[u].pb(v);
e[v].pb(u);
}
dfs(1,1);
for(auto x1:v1)
{
for(auto x2:v2)
s.insert(mk(min(x1,x2),max(x1,x2)));
}
for(int i=1;i<=n;i++)
if(fa[i])
s.erase(mk(min(i,fa[i]),max(i,fa[i])));
int x1,x2;
if(((int)(v1.size()*v2.size())-n+1)&1)cout<<"First\n"<<flush;
else
{
cout<<"Second\n"<<flush;
cin>>x1>>x2;
if(x1==-1&&x2==-1)return 0;
s.erase(mk(min(x1,x2),max(x1,x2)));
}
while(1)
{
pii p=*s.begin();
cout<<p.first<<" "<<p.second<<"\n"<<flush;
s.erase(p);
cin>>x1>>x2;
if(x1==-1&&x2==-1)break;
s.erase(mk(min(x1,x2),max(x1,x2)));
}
return 0;
}