P6255 ICPC2019 WF Dead-End Detector


P6255 ICPC2019 WF Dead-End Detector

想当年这道题被选为校内 ACM 赛前训练,结果是历城二中 57 级全灭,全场三个队,只有有一个队有分,并且只有一道题,非常惨烈,今天重新审视这道题,发现当时水平确实太低。

题意

这题题意比较绕,场上对题意也是一知半解,感到迷茫的话可以好好研究样例。

一个无向图,给死路的起点打标记,死路定义为从起点经过这条边后,无法不掉头走回起点。一条死路如果被一条有标记的死路不掉头地到达,那么这个死路的标记便不必打,求最少的标记数量和标记位置。

解法

首先,边双连通分量中的边一定不是死路,所以先缩点,考虑边双构成的树的树边即可。

我们发现,可以把问题转化为打 Tag 和删 Tag 两步,而这两个阶段是互不干扰的,也就是说我们可以先打 Tag,然后考虑如何删除多余的 Tag 即可。

说明这个结论,因为死路的定义是确定的,所以无论如何删除 Tag,一条路该是死路仍然是死路,所以打 Tag 删 Tag 互不影响。

删 Tag

因为删 Tag 需要的是有打 Tag 的边能不掉头经过这条边。能经过一条边也就是能经过这条边的起点,所以我们对于一个点讨论是否存在有这种边连入即可。

这个时候可能就会产生问题,就是删除 Tag 之后,原本连入某个点的有 Tag 的边的 Tag 被删除后,这条边的 Tag 就不能被删除了。但是,我们知道能否到达是可以传递的,所以如果有一条边的 Tag 被删除,它删除的 Tag 的正确性不会受到影响,所以我们对每次打 Tag,将终点记录一个 Deleted 标记,最后将有标记的所有点的出边的 Tag 都删掉即可。

但是有一种情况比较特殊,就是一条入边不能删除自己的回边的标记,这时因为走了入边再走回边就相当于是掉头了,但是如果有多个带 Tag 的入边,就无需考虑这个问题,因为多条入边的回边可以相互被删除。这些情况每个点记录一个唯一带 Tag 入边的地址,特判一下即可。

void Del(BCC* x) {
  EdgeB* Sid(x->Fst);
  while (Sid) { if (Sid != x->Dont) Sid->Deleted = 1; Sid = Sid->Nxt; }
}

值得注意的是:不能每次入边连进来就执行这个操作,必须离线处理,因为缩点之后是菊花图的数据可以卡到 \(O(n^2)\),打标记,最后统一删除可以保证每个点只回遍历一次它的连边。

打 Tag

我们发现,一条路径只要能通向一个非节点的双连通分量,在这个双连通分量里绕一圈,就可以不掉头地从原地反向出来。这个结论是接下来打 Tag 的基础。

对于一个连通块,随便找一个点为根,然后将边分成两类,连向父亲和连向儿子。

对于连向儿子的边,如果儿子的子树中有非节点的边双,那么不用打 Tag。如果没有,则说明这走条边后没法不掉头回到起点,则给连向儿子的边打 Tag。

如下是代码:

void DFSFall(BCC* x) {
  EdgeB* Sid(x->Fst);
  x->SubTree = x->Turn;
  while (Sid) {
    if (Sid->To != x->Fa) {
      DFSFall(Sid->To), x->SubTree |= Sid->To->SubTree;
      if (!(Sid->To->SubTree)) {
        Sid->Ava = 1;
        if (Sid->To->Dont) Sid->To->Dont = EB + 0x3f3f3f3f;
        else Sid->To->Dont = EB + ((Sid - EB) ^ 1);
        Sid->To->Deleted = 1;
      }
    }
    Sid = Sid->Nxt;
  }
}

然后是儿子连父亲的边,这种边需要讨论不打 Tag 的情况。

第一种是经过父亲往上走,能走到至少一个非节点边双,对于每个父亲,这种情况可以 DFS 过程中统计。

第二种情况,注意第二种情况都是建立在前面所说的情况不存在的前提下的。这时父亲存在儿子,它的子树中至少一个非节点边双。

第二种情况还需要讨论子树存在非节点边双的儿子的数量,当父亲只有一个儿子子树中存在非节点边双,则除了这个儿子以外都可以不打 Tag,但是需要在父亲连向这个特定的儿子的边上打 Tag。当有大于一个儿子的子树中有非节点边双,则按第一种情况处理即可。

接下来是程序实现:

void DFSRise(BCC* x) {
  BCC* Son(NULL);
  EdgeB* Sid(x->Fst);
  char More(0);
  while (Sid) {
    if ((Sid->To != x->Fa) && (Sid->To->SubTree)) More = (Son ? 1 : 0), Son = Sid->To;
    Sid = Sid->Nxt;
  }
  Sid = x->Fst;
  if (More || (x->Turn)) {
    while (Sid) {
      if (Sid->To != x->Fa) Sid->To->Turn = 1, DFSRise(Sid->To);
      Sid = Sid->Nxt;
    }
    return;
  }
  if (!Son) {
    while (Sid) {
      if (Sid->To != x->Fa) {
        DFSRise(Sid->To), EB[((Sid - EB) ^ 1)].Ava = 1, x->Deleted = 1;
        if (x->Dont) x->Dont = EB + 0x3f3f3f3f; else x->Dont = Sid;
      }
      Sid = Sid->Nxt;
    }
    return;
  }
  while (Sid) {
    if (Sid->To != x->Fa) {
      if (Sid->To == Son) {
        DFSRise(Sid->To), EB[((Sid - EB) ^ 1)].Ava = 1, x->Deleted = 1;
        if (x->Dont) x->Dont = EB + 0x3f3f3f3f; else x->Dont = Sid;
      }
      else Sid->To->Turn = 1, DFSRise(Sid->To);
    }
    Sid = Sid->Nxt;
  }
}

代码

接下来的内容就是人尽皆知的 Tarjan 了,直接缩点即可。

接下来给出代码省略缺省源的其余部分:

unsigned m, n, M;
unsigned A, C, D, t;
unsigned Cnt(0), Top(0), CntRoot(0), CntPrt(0);
struct Node;
struct BCC;
struct Edge {
  Node* To;
  Edge* Nxt;
}E[1000005];
struct EdgeIO {
  unsigned Frm, To;
  const inline char operator <(const EdgeIO& x) const { return (this->Frm ^ x.Frm) ? (this->Frm < x.Frm) : (this->To < x.To); }
}IO[1000005];
struct EdgeB {
  BCC* To;
  EdgeB* Nxt;
  EdgeIO UsedTo;
  char Ava, Deleted;
}EB[1000005];
struct BCC {
  BCC* Fa;
  EdgeB* Fst, * Dont;
  char Turn, SubTree, Deleted;
}B[500005], * Root[500005], * CntB(B);
struct Node {
  Edge* Fst;
  BCC* Bel;
  unsigned DFSr, Low;
}N[500005], * Stack[500005];
void Link(Node* x) {
  Edge* Sid(x->Fst);
  while (Sid) {
    if (Sid->To->Bel) {
      if (Sid->To->Bel < x->Bel) {
        EB[Cnt].UsedTo.Frm = x - N, EB[Cnt].UsedTo.To = Sid->To - N;
        EB[Cnt].Nxt = x->Bel->Fst, x->Bel->Fst = EB + Cnt, EB[Cnt++].To = Sid->To->Bel;
        EB[Cnt].UsedTo.Frm = Sid->To - N, EB[Cnt].UsedTo.To = x - N;
        EB[Cnt].Nxt = Sid->To->Bel->Fst, Sid->To->Bel->Fst = EB + Cnt, EB[Cnt++].To = x->Bel;
        Sid->To->Bel->Fa = x->Bel;
      }
    }
    Sid = Sid->Nxt;
  }
}
void Shrink(Node* x, Edge* No) {
  x->Low = x->DFSr = ++Cnt, Stack[++Top] = x;
  Edge* Sid(x->Fst);
  while (Sid) {
    if (Sid != No) {
      if (!(Sid->To->DFSr)) Shrink(Sid->To, E + ((Sid - E) ^ 1)), x->Low = min(x->Low, Sid->To->Low);
      else x->Low = min(x->Low, Sid->To->Low);
    }
    Sid = Sid->Nxt;
  }
  if (x->DFSr == x->Low) {
    ++CntB, CntB->Turn = (Stack[Top] != x);
    do { Stack[Top]->Bel = CntB; } while (Stack[Top--] != x);
  }
}
signed main() {
  n = RD(), m = RD(), M = (m << 1);
  for (unsigned i(0); i < m; ++i) IO[i].Frm = RD(), IO[i].To = RD();
  sort(IO, IO + m);
  for (unsigned i(0); i < M; i += 2) {
    C = IO[i >> 1].Frm, D = IO[i >> 1].To;
    E[i].Nxt = N[C].Fst, N[C].Fst = E + i, E[i].To = N + D;
    E[i ^ 1].Nxt = N[D].Fst, N[D].Fst = E + (i ^ 1), E[i ^ 1].To = N + C;
  }
  for (unsigned i(1); i <= n; ++i) if (!(N[i].DFSr)) Shrink(N + i, NULL), Root[++CntRoot] = CntB;
  Cnt = 0;
  for (unsigned i(1); i <= n; ++i) Link(N + i);
  for (unsigned i(1); i <= CntRoot; ++i) DFSFall(Root[i]), DFSRise(Root[i]);
  for (BCC* i(B + 1); i <= CntB; ++i) if (i->Deleted) Del(i);
  for (unsigned i(0); i < Cnt; ++i) if (EB[i].Ava && (!EB[i].Deleted)) IO[++CntPrt] = EB[i].UsedTo;
  sort(IO + 1, IO + CntPrt + 1);
  printf("%u\n", CntPrt);
  for (unsigned i(1); i <= CntPrt; ++i) printf("%u %u\n", IO[i].Frm, IO[i].To);
  system("pause");
  return Wild_Donkey;
}