bzoj 3887: [Usaco2015 Jan]Grass Cownoisseur题解

这道题我看了一个上午所有需要的相关知识点的视频,然后花了好久才打出来,果然是水平不够啊。。。。

为了以后还能看懂并记住这些知识点,特意写篇博客来记录一下。

优秀题解博客:http://blog.csdn.net/popoqqq/article/details/44081279

上面这篇博客写了好多我没见过的语法,看上去好厉害。。。。

首先,写这道题时,先要学会以下知识点:

  1.了解如何寻找强连通分量并进行缩点

  2.如何在DAG(有向无环图)中用拓扑找最短路

  3.如何使用链式前向星(会用vector的话可以自动忽略这一点)

题目链接:http://www.lydsy.com/JudgeOnline/problem.php?id=3887

   这道题我们可以发现如果我们走到一个强连通分量中,为了有尽可能多的点,我们会把它全都走一遍再决定从分量中的哪个点走出。

   所以我们可以先进行缩点 ,缩点后的点权为点所代表的原强连通分量中点的个数,这样我们就把它变成了更好处理的DAG。处理后的所有点总共能分成三类:

1.能到达点1的

2.从点1出发能到达的

3.和点1没有任何关系的

很显然我们可以删除第三类点,因为就算怎么翻转路径,第三类的点都和答案没有任何关系。而我们最终要反转的边在一开始一定是一条从第二类点到第一类点的边,我们把这样的边全部反转,把能达到点1的路径统统改成到达点n+1,这样问题就简化成了在DAG中找一条从点1到点n+1的点权和最大的路径,很明显,我们可以用拓扑+dp解决了;

可能有人会和我刚开始一样觉得万一我们最后找到的路径中有两条边被反转过,怎么办?其实可以发现这是不可能的,因为如果有,那么那两条被反转的边的中间的路径一定符合它们未反转之前就既能到达点1,也能被点1,可是这是DAG,绝不会存在环。

唔,这是大概思路,下面的程序实现与这个思路存在一点小小的差异,具体程序注释中讲吧。

#include<cstdio>
#include<iostream>
#include<cstring>
#include<stack>
using namespace std;
struct point{
    int to,next;
}edge[200000],edge1[200000][3];
int cnt,n,m,ans,t,tot;
int head[200000],head1[200000][3],d[200000][3],dfn[200000],low[200000],v[200000];
int node[200000],f[200000][3],size[200000],q[200000];
stack<int>s;
void add(int u,int v)
{
  edge[++cnt].to=v;
  edge[cnt].next=head[u];
  head[u]=cnt;
}
void add1(int u,int v,int ch)
//ch为1的时候表示原本的缩点后的图,2的时候表示所有边都已反向过的图
{
   if (ch==1) cnt++;
   d[v][ch]++; 
   edge1[cnt][ch].to=v;
   edge1[cnt][ch].next=head1[u][ch];
   head1[u][ch]=cnt;
}
void tarjan(int u)
{
  dfn[u]=low[u]=++t;//赋初值
  s.push(u);//放入栈中
  for (int i=head[u];i;i=edge[i].next)
      if (dfn[edge[i].to]==0)
//如果说这个点的dfn为0的话就表示这个点还未被搜索到过
      {
        tarjan(edge[i].to);
        low[u]=min(low[u],low[edge[i].to]);
    }
    else if (!v[edge[i].to])
//这个点没有出栈的话
     low[u]=min(low[u],dfn[edge[i].to]);
   if (dfn[u]==low[u])//如果这是一个强连通分量的根的话
   {
        tot++;//强连通分量数量加1,及缩点后的点数加1
        int now=0;
        while (now!=u)
        {
          now=s.top();//弹出栈
          s.pop();
       v[now]=true;//这个点已经出栈了
       node[now]=tot;//这个点属于哪一块强连通分量
       size[tot]++;    //这个强连通分量的大小加1
     }
   }
}
void find(int ch)
{
  f[node[1]][ch]=size[node[1]];
//ch依旧表示路是否被反向
//f[i][ch]数组则表示在当前的图,1到达[i]的路径上点权和最大为多少
  int ta=0; 
//当前队列的尾巴
  for (int i=1;i<=tot;i++) 
  if (d[i][ch]==0) q[++ta]=i;//如果说入度为0,加入队列
  while  (ta>0)//拓扑+dp就不详细解释了
  {
      int p=q[ta--];
      for (int i=head1[p][ch];i;i=edge1[i][ch].next)
      {
        f[edge1[i][ch].to][ch]=max(f[edge1[i][ch].to][ch],f[p][ch]+size[edge1[i][ch].to]);
      if (--d[edge1[i][ch].to][ch]==0) q[++ta]=edge1[i][ch].to;
    }
  }
}
int main()
{
  int x,y;
  scanf("%d%d",&n,&m);
  for (int i=1;i<=m;i++)    
  {
      scanf("%d%d",&x,&y);
      add(x,y);//连接未缩点之前的边
  }
  t=0;
  for (int i=1;i<=n;i++) if (!v[i]) tarjan(i);
//用tarjan求强连通分量并进行缩点
//v[i]表示这个点有无出栈,如果为false的话,说明这个点还未被访问或者说还在栈中
 cnt=0; 
  for (int i=1;i<=n;i++)
   for (int j=head[i];j;j=edge[j].next)
    if (node[i]!=node[edge[j].to])//此语句避免自环
    {
      add1(node[i],node[edge[j].to],1);//连边,之后搜索出的是1能到达的点
      add1(node[edge[j].to],node[i],2);//我们反向所有的边,这样搜索出来1能到达的点,其实是能到达1的点,这样就避免了构造n+1这个点
    }
  memset(f,0xef,sizeof f);//清空为一个很大的负数
  find(1);find(2);//搜索
  ans=size[node[1]];//开始答案的大小为1所在的强连通分量的大小
  for (int i=1;i<=n;i++)
   for (int j=head[i];j;j=edge[j].next)
//这里我们枚举所有未缩点前的边(当然这个取决于你自己)
//边的方向本来是node[i]——>node[edge[j].to]
//我们把它们反向,f[node[edge[j].to]][1]表示从点1到node[edge[j].to],最大点权和为多少
//f[node[i]][2]表示从node[i]到点1最大点权和为多少
//这样边反向后,我们就可以成功的构建一条
//不过因为f[node[edge[j].to]][1]和f[node[i]][2]都包含了1所在的强连通分量的点权,所以我们需要减去一个size[node[1]]。
//1——>node[edge[j].to]——>node[i]——>1的路径了
    if (node[i]!=node[edge[j].to])
    ans=max(ans,f[node[edge[j].to]][1]+f[node[i]][2]-size[node[1]]);
   printf("%d
",ans);
   return 0;   
} 
看完这道题好像收获了不少。。。
原文地址:https://www.cnblogs.com/2014nhc/p/6428588.html