9.21 图论测试 题解

9.21 图论测试 题解

A. 通讯(Kruskal 最小生成森林)

题意

给定平面上的 \(P\) 个哨所的坐标。哨所之间可以用无线电收发器或者卫星电话通话。每个哨所都有无线电收发器,但是配备卫星电话的哨所最多只能有 \(S\) 个。

求出最小的 用无线电收发器通话的最大距离 \(D\),使得每一对哨所之间至少有一条通话路径(直接的或者间接的)。

思路

Kruskal 贪心连接距离最近的 \(P-S\) 个哨所,剩下 \(S\) 个哨所用卫星电话连接。

Kruskal 算法最后一个连接的边即为最小的最大距离。

代码

#include <iostream>
#include <algorithm>
#include <cmath>
#include <iomanip>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define il inline
using namespace std;
typedef long long ll;
typedef double db;
int const N = 550;
int S, P;
int x[N], y[N];

il db cal(int a, int b, int c, int d) {
	return sqrt((a - c) * (a - c) + (b - d) * (b - d));
}

struct Edge{
	int u, v;
	db w;
} e[N * N];
int fa[N], cnt;
int getfa(int x) { return x == fa[x] ? x : fa[x] = getfa(fa[x]); }
db Kruskal() {
	int tot = 0;
	f(i, 1, P) fa[i] = i;
	sort(e + 1, e + cnt + 1, [](Edge p, Edge q) { return p.w < q.w; });
	f(i, 1, cnt) {
		int fx = getfa(e[i].u), fy = getfa(e[i].v);
		if (fx != fy) {
			fa[fx] = fy;
			++tot;
		}
		if (tot == P - S) {
			return e[i].w;
		}
	}
	return -1;
}

signed main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	
	cin >> S >> P;
	f(i, 1, P) {
		cin >> x[i] >> y[i];
		f(j, 1, i - 1) {
			e[++cnt] = {i, j, cal(x[i], y[i], x[j], y[j])};
		}
	}
	cout << fixed << setprecision(2) << Kruskal() << '\n';
	
	return 0;
}

B. 序列(拓扑排序)

题意

给定一个有向无环图,如果图的拓扑序唯一,输出一行一个字符串 YES,否则输出 NO

思路

在拓扑排序过程中,如果队列中同时存在多于一个点,即同时有多于一个点满足入度为 \(0\),那么说明拓扑序不唯一。

代码

#include <cstdio>
#include <queue>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define il inline
using namespace std;
int const N = 1e5 + 10;
int n, m, deg[N];
queue<int> q;

struct Edge{
	int to, nxt;
} e[N];
int head[N], cnt;
il void add(int from, int to) {
	e[++cnt].to = to, e[cnt].nxt = head[from], head[from] = cnt;
	return;
}

signed main() {
	
    scanf("%d%d", &n, &m);
	f(i, 1, m) {
		int x, y;
        scanf("%d%d", &x, &y);
		add(x, y);
		++deg[y];
	}
	
	f(i, 1, n) if (!deg[i]) q.push(i);
	while (!q.empty()) {
		int t = q.front();
		q.pop();
		if (!q.empty()) {
			puts("NO");
			return 0;
		}
		for (int i = head[t]; i; i = e[i].nxt) {
			int to = e[i].to;
			--deg[to];
			if (!deg[to]) q.push(to);
		}
	}
	puts("YES");
	
	return 0;
}

C. 移动(缩点 + 拓扑图上 DP)

题意

给定一个有向图,从图的 \(1\) 号节点出发,途中可以反向经过一条边,最后回到 \(1\) 号点。点和边可以重复经过。

求最多能经过的点的数量(同一个点被经过多次也只算一个点) 。

思路

由于点和边可以重复经过,所以图中的同一个强连通分量中的点可以看作同一点。

因此可以把有向图缩点变成一个 DAG(有向无环图),DAG 中点的权值即为 SCC(强连通分量)的大小。

以下讨论的均为在缩点后的 DAG 中。

设从 \(i\)\(j\) 最多能经过的点的数量为 \(f(i,j)\)。那么答案一定为 \(f(1,u)+f(v,1)\),其中 \(u,v\) 分别是图中某一条边的起点和终点。

我们可以在拓扑排序的过程中 dp 处理出所有 \(f(1,i)\);然后建反图,同样地处理出 \(g(1,j)\),那么 \(g(1,j)=f(j,1)\)

之后枚举正向图中的所有边 \(u\rightarrow v\),用 \(f(1,u)+g(1,v)-size(1)\) 更新答案,其中 \(size(1)\) 即为点 \(1\) 所在的 SCC 的大小。

代码

#include <iostream>
#include <stack>
#include <queue>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define il inline
using namespace std;
int const N = 1e5 + 10;
int n, m;

int head[N], cnt;
int head1[N], cnt1, fr[N];
int head2[N], cnt2;
struct Edge{
	int to, nxt;
} e[N << 1], e1[N << 1], e2[N << 1];
il void add(int from, int to) {
	e[++cnt].to = to, e[cnt].nxt = head[from], head[from] = cnt;
	return;
}
il void add1(int from, int to) {
	e1[++cnt1].to = to, e1[cnt1].nxt = head1[from], head1[from] = cnt1;
	fr[cnt1] = from;
	return;
}
il void add2(int from, int to) {
	e2[++cnt2].to = to, e2[cnt2].nxt = head2[from], head2[from] = cnt2;
	return;
}

int dfn[N], low[N], idx;
int scc[N], tot, v[N]; //v:SCC中的点的个数
stack<int> st;
bool inStack[N];
void Tarjan(int x) {
	dfn[x] = low[x] = ++idx;
	st.push(x);
	inStack[x] = true;
	for (int i = head[x]; i; i = e[i].nxt) {
		const int &to = e[i].to;
		if (!dfn[to]) {
			Tarjan(to);
			low[x] = min(low[x], low[to]);
		} else if (inStack[to]) {
			low[x] = min(low[x], dfn[to]);
		}
	}
	if (dfn[x] == low[x]) {
		int t;
		++tot;
		while (!st.empty()) {
			t = st.top();
			st.pop();
			inStack[t] = false;
			scc[t] = tot;
			++v[tot];
			if (t == x) break;
		}
	}
	return;
}

int dp1[N], dp2[N];
queue<int> q1, q2;
bool inQueue[N];
void Toposort1() {
	q1.push(scc[1]);
	dp1[scc[1]] = v[scc[1]];
	inQueue[scc[1]] = true;
	while (!q1.empty()) {
		int t = q1.front();
		q1.pop();
		inQueue[t] = false;
		for (int i = head1[t]; i; i = e1[i].nxt) {
			int to = e1[i].to;
			if (dp1[to] < dp1[t] + v[to]) {
				dp1[to] = dp1[t] + v[to];
				if (!inQueue[to]) {
					inQueue[to] = true;
					q1.push(to);
				}
			}
		}
	}
	return;
}
void Toposort2() {
	memset(inQueue, 0, sizeof inQueue);
	q2.push(scc[1]);
	dp2[scc[1]] = v[scc[1]];
	inQueue[scc[1]] = true;
	while (!q2.empty()) {
		int t = q2.front();
		q2.pop();
		inQueue[t] = false;
		for (int i = head2[t]; i; i = e2[i].nxt) {
			int to = e2[i].to;
			if (dp2[to] < dp2[t] + v[to]) {
				dp2[to] = dp2[t] + v[to];
				if (!inQueue[to]) {
					inQueue[to] = true;
					q2.push(to);
				}
			}
		}
	}
	return;
}

signed main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	
	cin >> n >> m;
	int x, y;
	f(i, 1, m) {
		cin >> x >> y;
		add(x, y);
	}
	f(i, 1, n) if (!dfn[i]) idx = 0, Tarjan(i);
	f(i, 1, n) {
		for (int k = head[i]; k; k = e[k].nxt) {
			int j = e[k].to;
			if (scc[i] == scc[j]) continue;
			add1(scc[i], scc[j]);
			add2(scc[j], scc[i]);
		}
	}
	Toposort1();
	Toposort2();
	int ans = v[scc[1]];
	f(i, 1, cnt1)
		if (dp1[e1[i].to] && dp2[fr[i]])
			ans = max(ans, dp1[e1[i].to] + dp2[fr[i]] - v[scc[1]]);
	cout << ans << '\n';
	
	return 0;
}

D. 删除(Floyd + 逆向思维)

题意

一个完全有向图,\(n\) 个节点,每条边上有一个权值,\(x\)\(y\) 的边的权值为 \(a[x,y]\)。给定 \(n\) 次删除操作,每次删去一个点,即删掉连入这个点和这个点连出的所有边。

求每次删点后图中现存的点任意两点间的最短路之和。即,设剩下的点集为 \(S\),求

\[\sum_{x\in S,y\in S,x\ne y} \mathrm{dis}(x,y) \]

其中 \(\mathrm{dis}(x,y)\) 表示 \(x\)\(y\) 的最短路。

对于 \(100\%\) 的数据:\(1 \le n \le 500,1 \le a[i,j] \le 10^5\ (i\ne j) ,a[i,i] = 0\)

思路

每次删点的过程,反过来即为不断加点的过程。

我们可以将删点操作离线,在反过来不断加点的过程中,用这次加的点去松弛所有边。

所以可以在 Floyd 枚举中间点的过程看成加点的过程,枚举要松弛的点的同时统计答案。

代码

#include <cstdio>
#include <cstring>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define g(x, y, z) for (int x = (y); (x) >= (z); --(x))
#define il inline
#define int ll
using namespace std;
typedef long long ll;
int const N = 550;
int n, a[N][N], d[N][N];
int del[N];
int sum, ans[N];

signed main() {
//	freopen("erase7.in", "r", stdin);
//	freopen("erase7.out", "w", stdout);
	
    scanf("%d", &n);
	if (n == 1) {
		putchar('0');
		return 0;
	}
	f(i, 1, n) f(j, 1, n) scanf("%d", &a[i][j]);
	g(i, n, 1) scanf("%d", &del[i]);
	f(ii, 1, n) {
		int i = del[ii];
		f(jj, 1, n) {
			int j = del[jj];
			d[ii][jj] = a[i][j];
		}
	}
	f(k, 1, n) {
		sum = 0;
		f(i, 1, n) f(j, 1, n) {
			if (d[i][j] > d[i][k] + d[k][j])
				d[i][j] = d[i][k] + d[k][j];
			if (i <= k && j <= k) sum += d[i][j];
		}
		ans[n - k + 1] = sum;
	}
	f(i, 1, n) printf("%d ", ans[i]);
    putchar('\n');
	
	return 0;
}
posted @ 2022-11-06 20:35  f2021ljh  阅读(11)  评论(0)    收藏  举报