• 【题解】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);                      //输出答案
    }
    
  • 相关阅读:
    登录注册页面切换
    LINUX系统日常使用命令
    find命令详解
    ssh命令详解
    tar命令详解
    route命令详解
    uname命令详解
    ps命令详解
    df命令详解
    virsh命令详解
  • 原文地址:https://www.cnblogs.com/g-mph/p/14632462.html
Copyright © 2020-2023  润新知