题目链接:【模板】缩点
前言
这是一道模板题。需要学习强连通分量和缩点,还有最短路径算法。
审题
给一张图,找一条路径使点权和最大。
思路
先用tarjan算法求出这张图中所有的强连通分量,将它们缩成一点,建一个缩点后的图。
这次题目让我们求这张图上的一条路径,使经过的点权之和最大。看到“最”,就会想到和最短路有关。但是这题求的是最大的点权之和,就需要考虑把最短路算法魔改一下。
这篇题解使用的是LPFA算法(Longest Path Fast Algorithm,发明者:沃·兹基硕德)
把SPFA算法的边权改为点权,松弛改为扩张:
if (dis[v] > dis[u] + siz[v]) dis[v] = dis[u] + siz[v];
->if (dis[v] < dis[u] + siz[v]) dis[v] = dis[u] + siz[v];
整个LPFA的代码如下:
void spfa(int s) { //LPFA开始
queue<int> que; //定义队列que
que.push(s); //将s push进队列
dis[s] = siz[s]; //将s出发的最长路的值初始化为它所在连通块的权值之和
used[s] = true; //标记s在队列中
while (!que.empty()) { //当队列不为空(即有值)时循环
int u = que.front(); //把u赋为que的队首元素
que.pop(); //删除que中的第一个元素
used[u] = false; //标记u不在队列中
for (int i = head[u]; ~i; i = edge[i].next) { //循环从u出发的所有边
int v = edge[i].to; //定义点v并把它赋值为这条边的终点
if (dis[v] < dis[u] + siz[v]) { //如果现在的“最长路”没有先走u的长度长
dis[v] = dis[u] + siz[v]; //就对路径进行“扩张”操作
if (!used[v]) { //如果点v不在队列中
used[v] = true; //标记它加入队列
que.push(v); //把它加入队列
}
}
}
}
for (int i = 1; i <= sccnt; i++) //循环每一个强连通分量,找dis(最长路)的最大值
ans = max(ans, dis[i]); //如果dis[i]比答案大,就让答案为dis[i]
}
因为需要求出权值的最大和,所以tarjan函数里面多了这样一句:
siz[sccnt] += val[u];
整个tarjan()
函数如下
void tarjan(int v) { //Tarjan算法
dfn[v] = low[v] = ++tot; //标记dfn[]访问顺序,还有low[]的初始值
sta.push(v); //让点v进栈
vis[v] = true; //标记这个点被访问过
for (int i = head[v]; ~i; i = edge[i].next) { //一直循环这个点每一个出度,直到-1表示没有了,这也是为什么memset head数组时要赋-1
int u = edge[i].to; //定义u并把它赋成这条边的终点
if (!dfn[u]) { //如果u没有被访问过
tarjan(u); //找下面这个点
low[v] = min(low[v], low[u]); //这个点low[v]的值就是当前low[]的值与找到的u点的low[]值
} else if (vis[u]) //如果u被访问过了,但是还在队列中
low[v] = min(low[v], dfn[u]); //low[v]就取这个点的low值与循环到的点u的dfn[u]的最小值
}
if (dfn[v] == low[v]) { //如果发现v这个点的dfn[]和low[]相等,说明这个点是一个强连通分量的“根”。
sccnt++; //scc(Strongly Connected Component), cnt(count),就是强连通分量的个数
int u; //定义u变量,作为栈顶元素
do {
u = sta.top(); //将u赋值为sta栈的栈顶元素
vis[u] = false; //将u弹出
sta.pop(); //同上
color[u] = sccnt; //将u标记为这个强连通分量里的点
siz[sccnt] += val[u]; //这个强连通分量的权值加上u这个点的权值
} while (v != u); //当v == u之后,结束循环
}
}
为了使每一个点都被访问到,tarjan()
的调用在循环中进行:
for (int i = 1; i <= n; i++) //循环每一个点
if (!dfn[i]) tarjan(i); //如果dfn[i]没有值,即这个点被没有访问过,需要访问;
//如果dfn[i]已经有一个值,说明这个点被访问过了,不用担心漏了,
//同时也为了节省时间,就不访问了。
建缩点后的图时,将原来的head[]
和edge[]
数组清空,循环每一条边,如果它的起点和终点不在一个强连通分量中,则连一条边:
for (int i = 1; i <= m; i++) //循环每一条边
if (color[from[i]] != color[to[i]]) //如果这条边的出发点和终止点不在同一个强连通分量中
add(color[from[i]], color[to[i]]); //就连一条边
代码
完整代码如下:
#include <bits/stdc++.h> //万能头文件
using namespace std; //名字空间,具体我也不知有啥用,但是有用到了iostream就得加这个,否则就得std::
const int maxN = 2e5 + 3; //数组大小
struct Edge {
int next, to; //用结构体存邻接表
} edge[maxN];
int head[maxN], dfn[maxN], low[maxN];
int color[maxN], val[maxN], siz[maxN];
int from[maxN], to[maxN], dis[maxN];
bool vis[maxN], used[maxN];
int cnt, tot, sccnt, ans, n, m;
stack<int> sta;
//以上定义了一包变量和数组
template<typename Tp> void read(Tp &x) {
char c = getchar(); //先输入一个字符
x = 0; //定义x并初始化为0,用来累计输入的数
while (!isdigit(c)) c = getchar(); //如果这个字符不是数字的话,就一直读下一个字符(本题没有负数情况)
do {
x = x * 10 + (c ^ 48); //先把x×10,然后与c^48相加
//十进制48转换为二进制为110000,而'0'到'9'都是11****,异或会使不同的位为1,相同的位为0
c = getchar(); //读下一个字符
} while (isdigit(c)); //循环条件为读到的为数字,则一直循环到读入的不是数字为止
}
void add(int from, int to) {
edge[++cnt].next = head[from], edge[cnt].to = to, head[from] = cnt;
}
void tarjan(int v) { //Tarjan算法
dfn[v] = low[v] = ++tot; //标记dfn[]访问顺序,还有low[]的初始值
sta.push(v); //让点v进栈
vis[v] = true; //标记这个点被访问过
for (int i = head[v]; ~i; i = edge[i].next) { //一直循环这个点每一个出度,直到-1表示没有了,这也是为什么memset head数组时要赋-1
int u = edge[i].to; //定义u并把它赋成这条边的终点
if (!dfn[u]) { //如果u没有被访问过
tarjan(u); //找下面这个点
low[v] = min(low[v], low[u]); //这个点low[v]的值就是当前low[]的值与找到的u点的low[]值
} else if (vis[u]) //如果u被访问过了,但是还在队列中
low[v] = min(low[v], dfn[u]); //low[v]就取这个点的low值与循环到的点u的dfn[u]的最小值
}
if (dfn[v] == low[v]) { //如果发现v这个点的dfn[]和low[]相等,说明这个点是一个强连通分量的“根”。
sccnt++; //scc(Strongly Connected Component), cnt(count),就是强连通分量的个数
int u; //定义u变量,作为栈顶元素
do {
u = sta.top(); //将u赋值为sta栈的栈顶元素
vis[u] = false; //将u弹出
sta.pop(); //同上
color[u] = sccnt; //将u标记为这个强连通分量里的点
siz[sccnt] += val[u]; //这个强连通分量的权值加上u这个点的权值
} while (v != u); //当v == u之后,结束循环
}
}
void spfa(int s) { //LPFA开始
queue<int> que; //定义队列que
que.push(s); //将s push进队列
dis[s] = siz[s]; //将s出发的最长路的值初始化为它所在连通块的权值之和
used[s] = true; //标记s在队列中
while (!que.empty()) { //当队列不为空(即有值)时循环
int u = que.front(); //把u赋为que的队首元素
que.pop(); //删除que中的第一个元素
used[u] = false; //标记u不在队列中
for (int i = head[u]; ~i; i = edge[i].next) { //循环从u出发的所有边
int v = edge[i].to; //定义点v并把它赋值为这条边的终点
if (dis[v] < dis[u] + siz[v]) { //如果现在的“最长路”没有先走u的长度长
dis[v] = dis[u] + siz[v]; //就对路径进行“扩张”操作
if (!used[v]) { //如果点v不在队列中
used[v] = true; //标记它加入队列
que.push(v); //把它加入队列
}
}
}
}
for (int i = 1; i <= sccnt; i++) //循环每一个强连通分量,找dis(最长路)的最大值
ans = max(ans, dis[i]); //如果dis[i]比答案大,就让答案为dis[i]
}
int main() {
memset(head, -1, sizeof(head)); //把head[]数组初始化为-1,具体原因见tarjan函数
read(n), read(m); //输入n、m
for (int i = 1; i <= n; i++) read(val[i]); //输入每一个点的权值
for (int i = 1; i <= m; i++) {
read(from[i]), read(to[i]); //输入每一条边的起点和重点
add(from[i], to[i]); //把起点和终点连一条边
}
for (int i = 1; i <= n; i++) //循环每一个点
if (!dfn[i]) tarjan(i); //如果dfn[i]没有值,即这个点被没有访问过,需要访问;
//如果dfn[i]已经有一个值,说明这个点被访问过了,不用担心漏了,
//同时也为了节省时间,就不访问了。
memset(head, -1, sizeof(head)); //将原来的head[]数组清空,以便重新建图
memset(edge, 0, sizeof(edge)); //将原来的edge[]数组清空,以便重新建图
for (int i = 1; i <= m; i++) //循环每一条边
if (color[from[i]] != color[to[i]]) //如果这条边的出发点和终止点不在同一个强连通分量中
add(color[from[i]], color[to[i]]); //就连一条边
for (int i = 1; i <= sccnt; i++) spfa(i); //循环每一个连通块(单独的一个点也是一个连通块),因为每一个连通块已经缩成一个点,所以就可以当作一个点来对待
printf("%d
", ans); //输出答案
}