7-1 连通分量
题意简述
给出一个 n n n 个结点 m m m 条边的无向图,求该无向图的连通分量的个数。 n ≤ 5 × 1 0 4 , m ≤ 1 0 5 n\leq 5\times 10^4, m\leq 10^5 n≤5×104,m≤105。时间限制 200 ms 200\text{ms} 200ms,空间限制 10 MB 10\text{MB} 10MB。
解题思路
您们,听说过 floodfill
吗?枚举图上的每个结点,如果到当前时刻,该节点还没有被访问过,就用DFS
遍历该节点所在的连通分量中的所有点,并令答案计数器加一;否则,什么都不做。
我们以题目给出的样例为例,演示这个算法的运行过程:
首先,我们对
1
1
1 号点进行 DFS
并把
1
,
2
,
3
1, 2, 3
1,2,3 三个点的 vis
标记置为
1
1
1。然后,由于结点
2
,
3
2,3
2,3 已经被访问过了,我们对
4
4
4 号结点进行 DFS
,并将
4
,
5
,
6
4, 5, 6
4,5,6 三个点的 vis
标记置为
1
1
1。然后,由于 结点
5
,
6
5, 6
5,6 已经被访问过了,算法结束,计算得到了两个连通分量。
递归实现
#include <cstdio>
#include <vector>
using namespace std;
const int maxn = 100000 + 6;
vector<int> nxt[maxn];
int vis[maxn];
void addedge(int f, int t) { /// 在图中添加一条无向边
nxt[f].push_back(t);
nxt[t].push_back(f);
}
void dfs(int x) { /// 使用 dfs 便利 结点 x 所在的连通分量的所有结点
if(vis[x]) return;
vis[x] = 1;
for(int i = 0; i < nxt[x].size(); i ++) {
int t = nxt[x][i];
if(!vis[t]) {
dfs(t);
}
}
}
int main() {
int n, m; scanf("%d%d", &n, &m);
for(int i = 1; i <= m; i ++) {
int u, v; scanf("%d%d", &u, &v); /// 输入并建边
addedge(u, v);
}
int ans = 0; /// 答案计数器
for(int i = 1; i <= n; i ++) {
if(!vis[i]) {
ans ++;
dfs(i);
}
}
printf("%d\n", ans);
}
BFS 做法
思路与上文相同,但是在遍历某个节点所在的连通分量的时候,我们采用 BFS
而不是 DFS
。
#include <cstdio>
#include <queue>
#include <vector>
using namespace std;
const int maxn = 50000 + 5;
vector<int> nxt[maxn];
void addedge(int f, int t) { ///
nxt[f].push_back(t);
nxt[t].push_back(f);
}
int vis[maxn];
void BFS(int rt) {
queue<int> q;
vis[rt] = 1;
q.push(rt);
while(!q.empty()) {
int x = q.front(); q.pop();
for(int i = 0; i < nxt[x].size(); i ++) {
int t = nxt[x][i];
if(!vis[t]) {
vis[t] = 1;
q.push(t);
}
}
}
}
int main() {
int n, m; scanf("%d%d", &n, &m);
for(int i = 1; i <= m; i ++) {
int u, v; scanf("%d%d", &u, &v);
addedge(u, v);
}
int ans = 0;
for(int i = 1; i <= n; i ++) {
if(!vis[i]) {
ans ++;
BFS(i);
}
}
printf("%d\n", ans);
return 0;
}
并查集做法
感谢衣然大佬的指导,让我想起来了还有这样一种利用并查集处理连通分量的算法。对于每一个结点,我们都给给他定义一个父亲。起初,每个节点的父亲都为它自己。每当我们读入一条边 ( u , v ) (u, v) (u,v),就意味着可能有两个连通块要进行合并。我们分别沿着父亲边找到 u u u 和 v v v 所在集合的代表元(我们用一棵树代表一个连通分量。根节点,也就是树上唯一一个以自己为父亲的结点,称为代表元。)为了让时间效率更优,我们在走过一条树上的路径时,可以把路径上经过的每一个点的父亲都直接指向当前连通分量的代表元,不难说明,一条深度为 h h h 的长链,经过一次访问后,就会变为 h h h 个长度为 1 1 1 的短链。
#include <cstdio>
const int maxn = 50000 + 5;
int fa[maxn], cnt; /// 每个结点在树上的父亲, cnt 记录当前连通块的个数
int find(int x) {
if(x != fa[x]) {
fa[x] = find(fa[x]); /// 路径压缩
}
return fa[x];
}
void link(int x, int y) {
x = find(x);
y = find(y); /// 找到 x y 对应连通块的代表元
if(x != y) {
fa[x] = y; /// 合并两个连通块
cnt --;
}
}
int main() {
int n, m; scanf("%d%d", &n, &m);
cnt = n;
for(int i = 1; i <= n; i ++) {
fa[i] = i; /// 初始化并查集
}
for(int i = 1; i <= m; i ++) {
int u, v; scanf("%d%d", &u, &v);
link(u, v);
}
printf("%d\n", cnt);
return 0;
}
7-2 整数拆分
给出两个正整数 n n n 和 k k k ,计算把 n n n 拆分成 k k k 个正整数的和的方案数并输出全部方案,其中 n , k ≤ 50 n,k\leq 50 n,k≤50。时间限制 100 ms 100\text{ms} 100ms,空间限制 1 M B 1MB 1MB。
解题思路
首先 5 = 2 + 3 , 5 = 3 + 2 5=2+3, 5 = 3+2 5=2+3,5=3+2 是同一种方案,为了避免重复统计,我们不妨在计算的过程中保证我们的到的拆分序列递增(实际上题目也要求按照递增顺序输出)。(p.s. 这里的递增指不下降)。因此,我们可以考虑采用回溯法,依次向 a r r [ 1 ] , a r r [ 2 ] , ⋯ , a r r [ k ] arr[1], arr[2], \cdots, arr[k] arr[1],arr[2],⋯,arr[k] 中填入整数,每填入一个整数后,就递归向下一个位置继续填入整数,直到恰好填入了 k k k 个整数时输出并统计答案。
代码实现
#include <cstdio>
int n, k;
int arr[52], cnt;
void dfs(int pos, int res) { /// 开始填入 arr[pos] ,当前剩余值为 res
if(pos == k + 1) { /// pos == k + 1 说明前 k 个位置已经恰好填满了
cnt ++;
for(int i = 1; i <= k; i ++) { /// 此时有 arr[1]+arr[2]+...+arr[k]=n 输出答案即可
printf("%d", arr[i]);
if(i == k) putchar('\n');
else putchar(' ');
}
}else {
if(pos != k) /// 如果 当前位置 不是最后一个位置
for(int i = arr[pos-1]; i <= res; i ++) { /// 在保证递增的前提下,填入下一个数
arr[pos] = i;
dfs(pos + 1, res - i);
arr[pos] = 0;
}
else {
if(res >= arr[pos - 1]) {
/// 如果当前位置是最后一个位置
/// 必须将 res 直接填入最后一个位置
/// 否则 arr[1]+arr[2]+...+arr[k] 将 不等于 n
/// 此时要判断数列递增 res >= arr[pos-1],以保证同种方案不会重复统计
arr[pos] = res;
dfs(pos + 1, 0);
}
}
}
}
int main() {
scanf("%d%d", &n, &k);
arr[0] = 1; /// 这样写是为了保证 arr[1] 大于等于 1
dfs(1, n); /// 开始填入第一个数,当前剩余数值为 n
printf("%d\n", cnt); /// 输出方案数
return 0;
}
7-3 数字变换
给定三种数值变换的方法
- x → x + 1 x \to x+1 x→x+1
- x → 2 x x \to 2x x→2x
- x → x − 1 x \to x-1 x→x−1
给定两个整数
x
,
y
x, y
x,y,保证
1
≤
x
,
y
≤
1
0
5
1\leq x, y\leq 10^5
1≤x,y≤105,试找到 从
x
x
x 变换到
y
y
y 的最少变化步数,并输出步数最少的方案,如果有多种方案,按照 变换1
小于 变换 2
小于 变换3
的原则按照字典序最小的合法操作序列进行操作。
解题思路
本质上就是一个有向无权图上的最短路问题。以样例为例: x = 2 , y = 14 x=2, y=14 x=2,y=14,下图中的圆点标出了从 2 2 2 到 14 14 14 的最短路。
BFS 实现
无权图的最短路问题通常可以使用 BFS
进行处理,我的做法并不是很好,因为我考虑了
x
x
x 与
y
y
y 小于零的情况,但是题目中保证
x
,
y
x, y
x,y 均为正数。
#include <cstdio>
#include <queue>
#include <cstring>
#include <map>
#include <stack>
using namespace std;
int DEP[400000 + 6]; /// dep[x] 表示 数字 x 在图中的层数
int PRE[400000 + 6];
stack<int> ary;
#define rin(A, B, C) (((A)<=(B))&&((B)<=(C))) /// 判断 是否满足 A <= B <= C
int main() {
int f, t; scanf("%d%d", &f, &t);
memset(DEP, 0xff, sizeof(DEP)); /// dep[x] = -1 表示 x 结点还没被访问过
int* dep = DEP + 200000 + 3;
int* pre = PRE + 200000 + 3;
/// 其实这种实现方式没有必要,我主要是想要支持一下 x y 为负数的情况
queue<int> q;
dep[f] = 0;
q.push(f);
while(!q.empty()) {
int x = q.front(); q.pop();
if(rin(-200000, x+1, 200000) && dep[x+1]==-1) {
dep[x+1] = dep[x] + 1;
pre[x+1] = x;
q.push(x+1);
if(x+1 == t) break;
}
if(rin(-200000, 2*x, 200000) && dep[2*x]==-1) {
dep[2*x] = dep[x] + 1;
pre[2*x] = x;
q.push(2*x);
if(2*x == t) break;
}
if(rin(-200000, x-1, 200000) && dep[x-1]==-1) {
dep[x-1] = dep[x] + 1;
pre[x-1] = x;
q.push(x-1);
if(x-1 == t) break;
}
}
printf("%d\n", dep[t]);
while(t != f) {
ary.push(t);
t = pre[t];
}
while(!ary.empty()) {
int x = ary.top(); ary.pop();
printf("%d", x);
if(!ary.empty()) putchar(' ');
else putchar('\n');
}
return 0;
}
7-4 旅行 I
题意简述
给出一个 n n n 个点 m m m 条边的带权无向图,求 从结点 s s s 到任意一个结点的边权和最短路,并求出从结点 s s s 到任意一个结点的最短路上,至多能经历多少个结点。
解题思路
我的解题思路不是很好,我先用 dijkstra
求出了源点
s
s
s 到每个结点的最短路,然后再把图中所有满足
d
i
s
[
v
]
=
d
i
s
[
u
]
+
w
(
u
,
v
)
dis[v]=dis[u]+w(u,v)
dis[v]=dis[u]+w(u,v) 的所有
(
u
,
v
)
(u,v)
(u,v) 重新建立了一张无权图,再用 spfa
算法 在这张无权图中跑从
S
S
S 开始到每个结点的最长路。
其实这个过程完全可以使用一次最短路求出,在计算最短路的过程中同时更新当前从根节点到某个节点的最短路上结点数的最大值即可。
代码实现
这是我第一次通过时的代码,方法比较低效,主要是因为当时我还没有证明出来在计算最短路同时计算 最短路最大边数 这个算法的正确性。
#include <cstdio>
#include <algorithm>
#include <queue>
#include <vector>
using namespace std;
const int maxm = 2*(100000 + 6);
const int maxn = 10000 + 6;
const int inf = 0x7f7f7f7fLL;
namespace spfa {
int vis[maxn], dis[maxn], fst[maxn];
int eto[maxm], nxt[maxm], ecnt;
void addedge(int f, int t) {
int id = ++ ecnt;
eto[id] = t;
nxt[id] = fst[f];
fst[f] = id;
}
void spfa(int S, int n) { /// 利用 spfa 在无权图上计算最长路
for(int i = 1; i <= n; i ++) {
dis[i] = -1;
}
dis[S] = 0;
queue<int> q; q.push(S); vis[S] = 1;
while(!q.empty()) {
int x = q.front(); q.pop(); vis[x] = 0;
for(int e = fst[x]; e; e = nxt[e]) {
int t = eto[e];
if(dis[t] < dis[x] + 1) {
dis[t] = dis[x] + 1;
if(!vis[t]) {
vis[t] = 1;
q.push(t);
}
}
}
}
for(int i = 1; i <= n; i ++) {
printf("%d", dis[i]);
if(i == n) putchar('\n');
else putchar(' ');
}
}
}
namespace dij {
int vis[maxn]; int dis[maxn]; int fst[maxn]; /// first[x] 表示 x 的第一条出边
int eto[maxm], cst[maxm], nxt[maxm], ecnt;
void addedge(int f, int t, int c) {
int id = ++ ecnt;
eto[id] = t;
cst[id] = c;
nxt[id] = fst[f];
fst[f] = id;
}
struct node {
int x; /// 点的编号
int dis; /// 某时刻 x 点 到 S 的最短路
};
struct cmp {
bool operator()(node A, node B) {
return A.dis > B.dis; /// 生成一个关于 dis 小根堆
}
};
priority_queue<node, vector<node>, cmp> pq;
void dij(int S, int n) { /// 利用堆优化 dijkstra 求源点到每个结点的最短路
for(int i = 1; i <= n; i ++) {
dis[i] = inf;
}
dis[S] = 0;
pq.push((node){S, 0});
while(!pq.empty()) {
node tmp = pq.top(); pq.pop();
int x = tmp.x;
if(vis[x]) {
continue;
}
vis[x] = 1;
for(int e = fst[x]; e; e = nxt[e]) {
int t = eto[e];
int c = cst[e];
if(dis[t] > dis[x] + c) {
dis[t] = dis[x] + c;
pq.push((node){t, dis[t]});
}
}
}
for(int i = 1; i <= n; i ++) {
printf("%d", dis[i]);
if(i == n) putchar('\n');
else putchar(' ');
}
for(int x = 1; x <= n; x ++) {
for(int e = fst[x]; e; e = nxt[e]) {
int t = eto[e], c = cst[e];
if(dis[t] == dis[x] + c) {
spfa::addedge(x, t);
}
}
}
spfa::spfa(S, n);
}
}
int main() {
int n, m, s; scanf("%d%d%d", &n, &m, &s);
for(int i = 1; i <= m; i ++) {
int u, v, w; scanf("%d%d%d", &u, &v, &w);
dij::addedge(u, v, w);
dij::addedge(v, u, w); /// 由于是无向图,记得双向建边
}
dij::dij(s, n);
return 0;
}
这是我第二次通过时的代码:
#include <cstdio>
#include <algorithm>
#include <queue>
#include <vector>
using namespace std;
const int maxm = 2*(100000 + 6);
const int maxn = 10000 + 6;
const int inf = 0x7f7f7f7fLL;
namespace dij {
int vis[maxn]; int dis[maxn]; int fst[maxn]; /// first[x] 表示 x 的第一条出边
int eto[maxm], cst[maxm], nxt[maxm], dep[maxn], ecnt;
void addedge(int f, int t, int c) {
int id = ++ ecnt;
eto[id] = t;
cst[id] = c;
nxt[id] = fst[f];
fst[f] = id;
}
struct node {
int x; /// 点的编号
int dis; /// 某时刻 x 点 到 S 的最短路
};
struct cmp {
bool operator()(node A, node B) {
return A.dis > B.dis; /// 生成一个关于 dis 小根堆
}
};
priority_queue<node, vector<node>, cmp> pq;
void dij(int S, int n) {
for(int i = 1; i <= n; i ++) {
dis[i] = inf;
}
dis[S] = 0;
pq.push((node){S, 0});
while(!pq.empty()) {
node tmp = pq.top(); pq.pop();
int x = tmp.x;
if(vis[x]) {
continue;
}
vis[x] = 1;
for(int e = fst[x]; e; e = nxt[e]) {
int t = eto[e];
int c = cst[e];
if(dis[t] > dis[x] + c) {
dis[t] = dis[x] + c;
dep[t] = dep[x] + 1; /// 更新最短路的同时更新 dep 数组即可
pq.push((node){t, dis[t]});
}else if(dis[t] == dis[x] + c) {
if(dep[x] + 1 >= dep[t]) {
dep[t] = dep[x] + 1; /// 相同长度的路径也可以更新 dep 数组
}
}
}
}
for(int i = 1; i <= n; i ++) {
printf("%d", dis[i]);
if(i == n) putchar('\n');
else putchar(' ');
}
for(int i = 1; i <= n; i ++) {
printf("%d", dep[i]);
if(i == n) putchar('\n');
else putchar(' ');
}
}
}
int main() {
int n, m, s; scanf("%d%d%d", &n, &m, &s);
for(int i = 1; i <= m; i ++) {
int u, v, w; scanf("%d%d%d", &u, &v, &w);
dij::addedge(u, v, w);
dij::addedge(v, u, w);
}
dij::dij(s, n);
return 0;
}