[暑假前集训]图论
2022年暑假前集训——图论
[题A]蜘蛛的网
解题思路
引入定义
图的边连通度:对于给定的无向图,如果至少删除\(K\)条边,可以使这个图不连通,则\(K\)称为图的边连通度;
无向图的割:有无向图\(G = (V, E)\),设\(C\)为图\(G\)中一些弧的集合,若从\(G\)中删去\(C\)中的所有弧能使图\(G\)不是连通图,称\(C\)图\(G\)的一个割;
全局最小割:包含的弧的权和最小的割,称为全局最小割。
显然,对于弧权均为1的图,求出全局最小割的大小,即可求出图的边连通度
而对于全局最小割的大小,有Stoer-Wagner算法可以求解
PS. Stoer-Wagner算法学习结合OI Wiki 和一篇博客
代码
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int INF = 1e9;
int sumw[310], n, m, edg[310][310];
bool vis[310], dlt[310];
int stcut(int &s, int &t){
memset(sumw, 0, sizeof(sumw));
memset(vis, 0, sizeof(vis));
int maxw, k, mincut;
for(int i = 1; i <= n; i++){
maxw = -1, k = -1;
for(int j = 1; j <= n; j++){
if(!dlt[j] && !vis[j] && sumw[j] > maxw)
k = j, maxw = sumw[j];
}
if(k == -1) return mincut;
s = t, t = k, mincut = maxw, vis[k] = 1;
for(int j = 1; j <= n; j++)
if(!dlt[j] && !vis[j]) sumw[j] += edg[j][k];
}
return mincut;
}
int Stoer_Wager(){
int mincut = INF, s, t;
for(int i = 1; i < n; i++){
mincut = min(mincut, stcut(s, t));
dlt[t] = 1;
for(int j = 1; j <= n; j++)
if(!dlt[j]) edg[s][j] = (edg[j][s] += edg[t][j]);
}
return mincut;
}
int main(){
ios::sync_with_stdio(false);
cin >> n >> m;
int u, v;
for(int i = 1; i <= m; i++){
cin >> u >> v;
edg[u][v] = edg[v][u] = 1;
}
cout << Stoer_Wager() << endl;
return 0;
}
[K题]居民都居住在房屋里
解题思路
给定图中任意两点间有且只有一条路径可达,显然给定的图是一颗树
则问题即\(m\)次查询树上两点间距离,即两点到其公共祖先的距离之和
使用常用的倍增法求LCA
#include <iostream>
#include <cstdio>
using namespace std;
struct Edges{ int v, nxt; } edg[1000010]; int fir[500010], ct = 0;
void add(int u, int v){
edg[++ct] = (Edges){v, fir[u]}, fir[u] = ct;
}
int dep[500010], fa[500010][22], lg[500010];
void dfs(int u, int frm){
fa[u][0] = frm, dep[u] = dep[frm] + 1;
for(int i = 1; i <= lg[dep[u]]; i++) fa[u][i] = fa[fa[u][i - 1]][i - 1];
for(int i = fir[u]; i; i = edg[i].nxt)
if(edg[i].v != frm) dfs(edg[i].v, u);
}
int lca(int x, int y){
if(dep[x] < dep[y]) swap(x, y);
while(dep[x] > dep[y]) x = fa[x][lg[dep[x] - dep[y]] - 1];
if(x == y) return x;
for(int k = lg[dep[x]] - 1; k >= 0; k--)
if(fa[x][k] != fa[y][k]) x = fa[x][k], y = fa[y][k];
return fa[x][0];
}
int main(){
int n, m, u, v, x, y, cofa;
scanf("%d%d", &n, &m);
for(int i = 1; i < n; i++){
scanf("%d%d", &u, &v);
add(u, v), add(v, u);
}
for(int i = 1; i <= n; i++) lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
dfs(1, 0);
for(int i = 1; i <= m; i++){
scanf("%d%d", &x, &y), cofa = lca(x, y);
printf("%d\n", dep[x] + dep[y] - 2 * dep[cofa]);
}
return 0;
}
[O题]纯白色的少年郎,如今只身在何方
解题思路
单源最短路模板
使用堆优化Dijkstra解决
代码
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
typedef long long ll;
struct Edges{int v, w, nxt;}edg[200010]; int ct = 0, fir[100010];
struct Nodes{
ll d; int id;
bool operator < (const Nodes &a) const {return d > a.d;}
};
void add(int u, int v, int w){
edg[++ct] = (Edges){v, w, fir[u]}, fir[u] = ct;
}
const ll INF = 1e18; ll dis[100010]; bool vis[100010];
priority_queue <Nodes> que;
void dijkstra(int s){
dis[s] = 0, que.push((Nodes){0, s});
while(!que.empty()){
int u = que.top().id; que.pop(); if(vis[u]) continue; vis[u] = 1;
for(int i = fir[u]; i; i = edg[i].nxt){
int v = edg[i].v;
if(dis[u] + (ll)edg[i].w < dis[v])
dis[v] = dis[u] + (ll)edg[i].w, que.push((Nodes){dis[v], v});
}
}
}
int main(){
int n, m, s, u, v, w;
scanf("%d%d%d", &n, &m, &s);
for(int i = 1; i <= m; i++) scanf("%d%d%d", &u, &v, &w), add(u, v, w);
for(int i = 1; i <= n; i++) dis[i] = INF;
dijkstra(s);
for(int i = 1; i <= n; i++)
printf("%lld\n", dis[i] != INF ? dis[i] : -1);
return 0;
}
[C题]魔法少女
解题思路
看作有一个\((N+1)*(M+1)\)的方阵图,每个小方形的顶点为图中的点,对角线为图中的边。
左上角点序号为\(1\),右下角点序号为\((N+1)*(M+1)\),
对于读入的符号,看作权值为0的边,而未读入的对角线看作权值为1的边,
在上述图中求起点到终点的最短路即为所求答案。
代码
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
struct Edges{int v, w, nxt;}edg[1000010]; int ct = 0, fir[300010];
struct Nodes{
int d, id;
bool operator < (const Nodes &a) const {return d > a.d;}
};
void add(int u, int v, int w){
edg[++ct] = (Edges){v, w, fir[u]}, fir[u] = ct;
edg[++ct] = (Edges){u, w, fir[v]}, fir[v] = ct;
}
int dis[300010]; const int INF = 1e9; bool vis[300010];
priority_queue <Nodes> que;
void dijkstra(){
dis[1] = 0; que.push((Nodes){0, 1});
while(!que.empty()){
int u = que.top().id; que.pop(); if(vis[u]) continue; vis[u] = 1;
for(int i = fir[u]; i; i = edg[i].nxt){
int v = edg[i].v;
if(dis[u] + edg[i].w < dis[v])
dis[v] = dis[u] + edg[i].w, que.push((Nodes){dis[v], v});
}
}
}
int main(){
int n, m; char c;
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
scanf(" %c", &c);
add((i - 1) * (m + 1) + j, i * (m + 1) + j + 1, (int)c == 92 ? 0 : 1);
add(i * (m + 1) + j, (i - 1) * (m + 1) + j + 1, c == '/' ? 0 : 1);
}
}
int tag = (n + 1) * (m + 1);
for(int i = 1; i <= tag; i++) dis[i] = INF;
dijkstra();
if(dis[tag] != INF) printf("%d\n", dis[tag]);
else printf("NO SOLUTION\n");
return 0;
}
[L题]最小生成树
解题思路
实际上是最小生成树计数问题
首先考虑生成树计数问题,需要引入矩阵树定理
\(Matrix-Tree \ Theorem:\)设图\(G=(V,E),\)构造\(G\)的拉普拉斯矩阵\(L,\)则\(G\)的生成树的个数等于\(detL_0,\)其中\(L_0\)是去掉\(L\)第\(i\)行第\(i\)列的子矩阵(\(i\)任意)。
其中\(L(G)\)是一个\(n\times n\)矩阵(\(n\)为节点数),且\[L_{i,j}= \begin{cases} -m_{i,j}, \ i\ne j\ (m_{i,j}是v_i与v_j之间的边数)\\ deg(v_i),\ i=j \end{cases}\]
详细证明见知乎文章,其中引理\(Binet-Cauchy\ 定理\)的证明见百度百科
然后考虑最小生成树的性质,若按照边的权值大小将边分为若干组,在\(Kruskal\)求最小生成树的过程中,从每一组边中选择的边数必相等,且每处理完一组边,将这组中被选择的边加入图中后,图的连通性是固定的。
那么考虑\(Kruska\)l的过程,从最小权值的边集开始,将这些边全部加入图中,形成若干连通块,对这若干连通块分别利用矩阵树定理求最小生成树的数量(记为\(t_i\)),然后将每一个连通块缩点,再处理权值第二小的边集,重复上述过程,直至\(Kruskal\)结束。则$ \prod t_i$即为全图最小生成树个数。
\(Ps.\)求矩阵的行列式要用到高斯消元法,时间复杂度为\(O(n^3)\),由于题目要求取模,且计算过程涉及到除法,正常情况下要用到乘法逆元。本题中模数为合数,部分数无逆元,所以用辗转相除法消元
- 缩点过程用并查集实现,遍历连通块过程中将所有点指向同一父节点,后续加边过程中将连通块内点用这一父节点代替
- 对于高斯消元的相关学习见某博客
代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
struct Edges{ int u, v, w; }e[1010];
struct realEdges{ int v, w, nxt; } edg[2020]; int ct = 0, fir[1010];
int n, m, L[110][110], num, fa[110], pos[110]; const int p = 10000;
bool cmp(Edges a, Edges b){ return a.w < b.w; }
int find(int x){ return (fa[x] ? fa[x] = find(fa[x]) : x); }
void add(int i){
int u = find(e[i].u), v = find(e[i].v);
edg[++ct] = (realEdges){v, e[i].w, fir[u]}, fir[u] = ct;
edg[++ct] = (realEdges){u, e[i].w, fir[v]}, fir[v] = ct;
}
void dfs(int u, int w, int fu){
if(u != fu) fa[u] = fu;
pos[u] = num++;
for(int i = 0; i <= pos[u]; i++) L[i][pos[u]] = L[pos[u]][i] = 0;
for(int i = fir[u]; edg[i].w == w; i = edg[i].nxt){
int v = edg[i].v;
if(pos[v] == -1) dfs(v, w, fu);
L[pos[u]][pos[v]]--, L[pos[u]][pos[u]]++;
}
}
int solve(int L[110][110], int n){
int det = 1; n--;
for(int i = 0; i < n; i++){
for(int j = i + 1; j < n; j++){
while(L[j][i]){
int t = L[i][i] / L[j][i];
for(int k = i; k < n; k++)
L[i][k] = (L[i][k] - 1ll * L[j][k] * t % p + p) % p;
swap(L[i], L[j]), det *= -1;
}
if(!L[i][i]) return 0;
}
det = 1ll * det * L[i][i] % p;
}
return (det + p) % p;
}
int ans = 1;
void Divide(){
for(int i = 0; i < m; ){
int j = i;
while(j < m && e[j].w == e[i].w) j++;
for(int k = i; k < j; k++) add(k);
for(int k = 1; k <= n; k++) pos[k] = -1;
for(int k = 1; k <= n; k++) if(find(k) == k){
num = 0, dfs(k, e[i].w, k);
ans = ans * solve(L, num) % p;
}
i = j;
}
}
int main(){
ios::sync_with_stdio(false);
int u, v, w;
cin >> n >> m;
for(int i = 0; i < m; i++) cin >> e[i].u >> e[i].v >> e[i].w;
sort(e, e + m, cmp);
Divide();
int t = 0;
for(int i = 1; i <= n; i++) t += (find(i) == i);
printf("%d\n", t > 1 ? 0 : ans);
return 0;
}
[J题]tarjan
解题思路
\(tarjan\)求割点、桥以及点双的问题模板
\(tarjan\)的\(dfs\)过程中,当前节点为\(u\),目标子节点为\(v\),所在树根节点为\(root\)
若\(low_v \ge dfn_u \land u \ne root\),则\(u\)为割点,所维护栈中从栈顶到\(v\)的点及点\(u\)构成点双
若存在两个及以上子节点满足\(low_v \ge dfn_u\),则\(root\)为割点
若\(low_v > dfn_u\)则边\(u \to v\)是桥
代码
#include <iostream>
#include <cstdio>
using namespace std;
struct Edges { int v, nxt; }edg[1000010]; int ct = 0, fir[1010];
void add(int u, int v) { edg[++ct] = (Edges){v, fir[u]}, fir[u] = ct; }
int dfn[1010], low[1010], fa[1010], vn, en, col[1010];
int sta[1000010], til, cirmax, cirn, cir[1010], t2, e[1010][1010];
void countcir(){
int ret = 0;
for(int i = 1; i <= t2 - 1; i++){
for(int j = i + 1; j <= t2; j++){
if(e[cir[i]][cir[j]]) ret++;
}
}
cirmax = max(cirmax, ret);
}
void tarjan(int u, int rt, int k){
dfn[u] = low[u] = ++ct; int flg = 0;
for(int i = fir[u]; i; i = edg[i].nxt){
int v = edg[i].v; if(v == fa[u]) continue;
if(!dfn[v]){
fa[v] = u, sta[++til] = i;
tarjan(v, rt, i), low[u] = min(low[u], low[v]);
en += (low[v] > dfn[u]);
if(low[v] >= dfn[u]){
t2 = 0, flg++;
while(sta[til] != i) cir[++t2] = edg[sta[til--]].v;
til--, cir[++t2] = v, cir[++t2] = u, countcir(), cirn++;
}
}else low[u] = min(low[u], dfn[v]);
}
vn += (u == rt && flg > 1 || u != rt && flg);
}
int main(){
ios::sync_with_stdio(false);
int n, m, u, v;
cin >> n >> m;
for(int i = 1; i <= m; i++){
cin >> u >> v;
add(u, v), add(v, u);
e[u][v] = e[v][u] = 1;
}
ct = 0;
for(int i = 1; i <= n; i++) if(!dfn[i]) tarjan(i, i, 0);
cout << vn << ' ' << en << ' ' << cirn << ' ' << cirmax << endl;
return 0;
}

浙公网安备 33010602011771号