题解
A - Cow Contest
题意
有
n
n
n头牛,知道
m
m
m对强弱关系,用一对数
(
a
,
b
)
(a,b)
(a,b)表示,意思是
a
a
a能战胜
b
b
b。强弱关系可以传递,如
(
a
,
b
)
(a,b)
(a,b)且
(
b
,
c
)
(b,c)
(b,c)则
(
a
,
c
)
(a,c)
(a,c)。求能确定排名的牛数量。
1
≤
n
≤
100
,
1
≤
m
≤
4500
1\leq n \leq 100,1 \leq m \leq 4500
1≤n≤100,1≤m≤4500
思路
对于一头牛,能确定其排名相当于,比其弱加比起强的牛数量等于 n − 1 n-1 n−1。强弱关系可用矩阵表示, g i , j = 1 g_{i,j}=1 gi,j=1表示 i i i比 j j j强, = 0 =0 =0则不确定。用floyd传递闭包即可。
代码
#include <cstdio>
#include <cstdlib>
#include <cctype>
#define fo(i, x, y) for (int i = x; i <= (y); ++i)
#define fd(i, x, y) for (int i = x; i >= (y); --i)
using namespace std;
const int maxn = 100 + 5;
int n, m;
bool g[maxn][maxn];
int getint()
{
char ch;
int res = 0, p;
while (!isdigit(ch = getchar()) && ch != '-');
p = ch == '-'? ch = getchar(), -1 : 1;
while (isdigit(ch))
res = res * 10 + ch - '0', ch = getchar();
return res * p;
}
int main()
{
n = getint(); m = getint();
fo(i, 1, m)
{
int u, v;
u = getint(); v = getint();
g[u][v] = true;
}
fo(k, 1, n)
fo(i, 1, n)
fo(j, 1, n)
g[i][j] |= g[i][k] & g[k][j];
int ans = 0;
fo(i, 1, n)
{
int cnt = 0;
fo(j, 1, n) cnt += g[j][i] + g[i][j];
ans += cnt == n - 1;
}
printf("%d\n", ans);
return 0;
}
D - Shortest Cycle
题意
给定长度为
n
n
n的序列
{
a
i
}
\{a_i\}
{ai},若
a
i
&
a
j
≠
0
(
i
≠
j
)
a_i \& a_j \neq 0(i \neq j)
ai&aj=0(i=j),则连一条无向边
(
i
,
j
)
(i,j)
(i,j),求最小环。
1
≤
n
≤
1
0
5
,
0
≤
a
i
≤
1
0
18
1 \leq n \leq 10^5,0 \leq a_i \leq 10^{18}
1≤n≤105,0≤ai≤1018
思路
无向图的最小环长度是3,而且若个第二进制位上存在3个数有1,则答案就为3,因为这三个数&起来不为0,可以两两连边。在
1
0
18
10^{18}
1018的数据范围下,每个数最多只有60个二进制位,由抽屉原理可知,若
n
>
60
∗
2
n>60*2
n>60∗2则肯定存在一个位使得有三个数在该位上是1,直接输出3即可。
现在只用考虑
n
≤
120
n \leq 120
n≤120的情况就好。求最小环是floyd的经典应用,可以在用第
k
k
k个点更新最短路前,枚举两个点
i
,
j
i,j
i,j,用
i
i
i到
j
j
j的最短路加上边
(
i
,
k
)
(i,k)
(i,k)和
(
j
,
k
)
(j,k)
(j,k)去更新答案,这样保证枚举的环不会有重边。
代码
#include <bits/stdc++.h>
#define fo(i, x, y) for (int i = x; i <= (y); ++i)
#define fd(i, x, y) for (int i = x; i >= (y); --i)
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 5, inf = 1e9;
int n;
ll p[maxn];
int a[125][125], f[125][125];
int getint()
{
char ch;
int res = 0, p;
while (!isdigit(ch = getchar()) && ch != '-');
p = ch == '-'? ch = getchar(), -1 : 1;
while (isdigit(ch))
res = res * 10 + ch - '0', ch = getchar();
return res * p;
}
int main()
{
n = getint();
int tot = 0;
fo(i, 1, n)
{
ll x;
scanf("%lld", &x);
if (x) p[++tot] = x;
}
n = tot;
if (n > 120) {printf("3\n"); return 0;}
fo(i, 1, n)
fo(j, 1, n)
if (i != j && (p[i] & p[j])) a[i][j] = f[i][j] = 1;
else a[i][j] = f[i][j] = inf;
int ans = inf;
fo(k, 1, n)
{
fo(i, 1, k - 1)
fo(j, i + 1, k - 1)
if (f[i][j] < inf && a[j][k] < inf && a[k][i] < inf)
ans = min(ans, f[i][j] + a[j][k] + a[k][i]);
fo(i, 1, n)
fo(j, 1, n)
f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
}
if (ans < inf) printf("%d\n", ans);
else printf("-1\n");
return 0;
}
CF1108F MST Unification
这题是排位赛的题,个人觉得比较有价值就放进来了。
传送门
题意
有 n n n个点和 m m m条无向边,每条边有边权,现可对边进行不限次边权+1操作,问使得最小生成树唯一的最少操作次数。
思路
假设现在弄出了任意一棵最小生成树,观察哪些边需要被操作。
现把树外一条边加到树内,设其为
(
u
,
v
)
(u,v)
(u,v),则会形成一个环,如果这条边的边权大于树路径
(
u
,
v
)
(u,v)
(u,v)上任意一边,则这条边一定不会被任意一棵最小生成树包含,对答案无影响;
剩下的情况只有
(
u
,
v
)
(u,v)
(u,v)边权等于树路径
(
u
,
v
)
(u,v)
(u,v)上的最大值,则这条边可以与树路径
(
u
,
v
)
(u,v)
(u,v)上权值与其相等的边互换,从而形成多棵最小生成树,此时只要把边
(
u
,
v
)
(u,v)
(u,v)权值+1,即可使
(
u
,
v
)
(u,v)
(u,v)不被任意最小生成树包含。
所以此时最直接的做法是任意建一棵最小生成树,然后遍历一遍树外边,用类似树上倍增之类的方法快速查询树路径的最大边,若等于当前枚举的边则ans++。
结合以上分析再来观察kruskal算法,由于对答案有贡献的边与边权重复出现有关,对于边权相等的边一批批地考虑,假设现在准备往并查集加入边权为
w
w
w的边,设其中一条边为
(
u
,
v
)
(u,v)
(u,v),则加入该边时发现
u
,
v
u,v
u,v已经在同一集合说明已经与形成环,且路径
(
u
,
v
)
(u,v)
(u,v)上的边小于
w
w
w,则该边对答案无影响,可删除,否则可能对答案有贡献。然后把可能对答案有贡献的边加入并查集,在加入过程中如果发现边两端点已经联通则说明路径上肯定有刚加入边权为
w
w
w的边,此时ans++。这题就做完了,只需要稍微修改一下kruskal即可。
代码
#include <bits/stdc++.h>
#define fo(i, x, y) for (int i = x; i <= (y); ++i)
#define fd(i, x, y) for (int i = x; i >= (y); --i)
using namespace std;
const int maxn = 2e5 + 5, maxm = 2e5 + 5;
int n, m;
int fa[maxn], siz[maxn], cho[maxm];
bool flag[maxm];
struct edge{int u, v, w;}e[maxm];
int getint()
{
char ch;
int res = 0, p;
while (!isdigit(ch = getchar()) && ch != '-');
p = ch == '-'? ch = getchar(), -1 : 1;
while (isdigit(ch))
res = res * 10 + ch - '0', ch = getchar();
return res * p;
}
bool cmp(edge a, edge b) {return a.w < b.w;}
int getfa(int x)
{
return x == fa[x]? x : fa[x] = getfa(fa[x]);
}
bool mer(int x, int y)
{
int fx = getfa(x), fy = getfa(y);
if (fx == fy) return false;
if (siz[fx] < siz[fy]) fa[fx] = fy, siz[fy] += siz[fx];
else fa[fy] = fx, siz[fx] += siz[fy];
return true;
}
int main()
{
n = getint(); m = getint();
fo(i, 1, m)
{
e[i].u = getint(); e[i].v = getint(); e[i].w = getint();
}
sort(e + 1, e + 1 + m, cmp);
fo(i, 1, n) fa[i] = i, siz[i] = 1;
int cnt = 0, ans = 0;
fo(i, 1, m)
{
int j = i;
while (j + 1 <= m && e[j + 1].w == e[i].w) j++;
fo(k, i, j)
{
int fu = getfa(e[k].u), fv = getfa(e[k].v);
if (fu != fv) flag[k] = true;
}
fo(k, i, j)
if (flag[k])
ans += !mer(e[k].u, e[k].v);
i = j;
}
printf("%d\n", ans);
return 0;
}
知识总结
此专题训练的模板题比较多,而题解后两题是相对灵活的题目;题解第二题结合抽屉原理的知识,第三题则要理解透彻最小生成树的生成过程,和kruskal的算法原理。
现在的图论水平还是比较差劲,之后还需要复习/学习高级图论算法和知识。