【题解】P3387 【模板】缩点

题目链接:【模板】缩点

前言

这是一道模板题。需要学习强连通分量和缩点,还有最短路径算法

审题

给一张图,找一条路径使点权和最大。

思路

先用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\n", ans);                      //输出答案
}
posted @ 2021-04-08 15:19  淼畔  阅读(319)  评论(0编辑  收藏  举报