zoukankan      html  css  js  c++  java
  • 【题解】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
    ", ans);                      //输出答案
    }
    
  • 相关阅读:
    巴洛克式和哥特式的区别
    推荐阅读书籍,是时候再行动起来了。
    AtCoder ABC 159F Knapsack for All Segments
    AtCoder ABC 159E Dividing Chocolate
    AtCoder ABC 158F Removing Robots
    AtCoder ABC 158E Divisible Substring
    AtCoder ABC 157F Yakiniku Optimization Problem
    AtCoder ABC 157E Simple String Queries
    AtCoder ABC 157D Friend Suggestions
    AtCoder ABC 156F Modularness
  • 原文地址:https://www.cnblogs.com/g-mph/p/14632462.html
Copyright © 2011-2022 走看看