进阶图论学习笔记

零、杂项

  • P2323 [HNOI2006] 公路修建问题:这里在分析贪心问题时使用了先取满限制条件再试着替换优化的“倒推法”,觉得很妙。

  • P2491 [SDOI2011] 消防:蓝书上原题,可以利用树的直径的性质,优化极简的 \(O(n)\)。(感觉我可以做一个树的直径与树上贪心的专题。。。)



二、2-SAT

基础内容



三、最小树形图

1. 朱刘算法

reference

算法的核心思想:尝试使用 DAG 的方式贪心。如果贪出环了,就使用 反悔贪心 的方法把环 缩为一个点,迭代执行直到图上不存在环。

时间复杂度:\(O(nm)\)

算法流程:

事实证明如果还不能独立写出这个算法,只能说步骤总结得不够详细!

  • 输入,按照 边集数组 的方式存储图。

  • 开始迭代:

    • 初始化。

    • 为每一个点找到最小入边。注意根不应有最小入边,还应注意此处的边是否在同一 scc 内(即自环)。

    • 将每一条新入边加入答案。如果此时发现有除了根以外的点没有入边,则无解。

    • 找环(相当于在 内向基环树 上找环):

      • 每次从一个点开始沿着边走,标记走到的点,直到找到被标记过的点或者走到

      • 如果当前被标记过的点为从不同点出发标记的,跳过。

      • 否则说明我们找到了一个新环。建立一个新的 scc,再走一遍环,将所有点加入。

    • 如果这一步没有找到任何环,说明贪心成功,可以终止迭代输出答案了。

    • 否则我们还需要缩点。先对所有不在环内的点也建立 scc 然后对于每一条边,将其边权修改为与环内边权之差。

(总之就是当心一下根的特判,其余细节其实不多。)

例题:


2. Tarjan

以后我实在闲着没事再来学这个。。。



四、网络流

link



五、二分图

下面内容全部都是从蓝书抄的


1. 定义

一张无向图的 \(n\) 个节点可以分成 \(A, B\) 两个非空集合,且同一集合内的点之间都没有边相连,那么称其为二分图,\(A, B\) 分别成为二分图的左部和右部。


2. 二分图判定

用 DFS 染色法。将第一个点染为黑色,并将其相邻的点染为白色;往下递归,将每个点相邻的点染为相反的颜色,如果矛盾说明不是二分图。


3. 二分图最大匹配

  • 算法

    • 匈牙利算法:核心在寻找匹配边、非匹配边交错出现的“增广路”。时间复杂度 \(O(nm)\)

      点击查看代码
      #include<bits/stdc++.h>
      using namespace std;
      
      const int MAXN = 505, MAXM = 5e4+5;
      int n, m, e, head[MAXN], mth[MAXN];
      bool vst[MAXN];
      //head 仅用来标记左部点:右部点不需要进行尝试,只需要找到 match 的对象即可
      //vst, mth 仅用来标记右部点:对于任意左部点,只要对应的右部点没有 match,则一定没访问
      
      struct node{
      	int to, nxt;
      } edge[MAXM];
      
      inline void Add_edge(int i, int from, int to){
      	edge[i].to = to;
      	edge[i].nxt = head[from];
      	head[from] = i;
      	return;
      }
      
      inline bool DFS(int x){
      	for(int i = head[x]; i; i = edge[i].nxt){
      		int to = edge[i].to;
      		if(vst[to])	continue;
      		vst[to] = true;
      		if(!mth[to] or DFS(mth[to]))	{mth[to] = x; return true;}
      	}
      	return false;
      }
      
      int main(){
      	scanf("%d%d%d", &n, &m, &e);
      	for(int i = 1; i <= e; i++){
      		int ui, vi;	scanf("%d%d", &ui, &vi);
      		Add_edge(i, ui, vi);
      	}
      	int ans = 0;
      	for(int i = 1; i <= n; i++){
      		memset(vst, false, sizeof(vst));
      		if(DFS(i))	++ans;
      	}
      	cout<<ans;
      	
      	return 0;
      }
      
    • 网络最大流:见 ,复杂度可以做到 \(O(m \sqrt{n})\),但好像专门卡匈牙利放这个的题目并不多。

  • 建模要素

    • 0 要素:节点可分为独立的两个集合,每个集合内部有 0 条边。

    • 1 要素:每个节点只能和 1 条匹配边相连。

  • 题目

    • 棋盘覆盖:以格子为节点,以黑/白格子为两个集合,以邻线为边。

    • 车的放置:以行/列为两个集合,以格子为边。


4. 二分图最大多重匹配

两种主要解决方案:

  1. 拆点。这也是图论问题中的常见转化方法。复杂度 \(O(nwm)\)\(w\) 系最多拆为几个点)。

  2. 网络最大流。复杂度 \(O(n^2m)\)


5. 二分图带权匹配

即权值和最大/小的一组二分图最大匹配。【注意:一定得先为最大匹配。】

两种主要解决方案:

  1. KM 算法。只能用于最大匹配为 完备匹配 时。时间复杂度 \(O(n^2m)\),用 BFS 优化后可以达到 \(O(nm)\)

    这个算法的关键科技在“顶标”的设计:每个节点都有一个“顶标”,并且对于一条边 \((i, j)\),我们始终维护其满足 \(v(i)+v(j) \ge w(i, j)\)。主要框架还是和匈牙利差不多。至于优化就是把匈牙利改成 BFS 写法即可。

    点击查看代码
    int n, m, head[MAXN], mth[MAXN];
    ll la[MAXN], lb[MAXN], d[MAXN];//顶标 和 辅助数组d 
    bool va[MAXN], vb[MAXN];//标记是否在失配树内 
    
    inline bool DFS(int x){
    	va[x] = true;
    	for(int i = head[x]; i; i = edge[i].nxt){
    		int to = edge[i].to;
    		if(vb[to])	continue;
    		if(la[x]+lb[to] != edge[i].wi){
    			d[to] = min(d[to], la[x]+lb[to]-edge[i].wi);
    			//to 可能在 DFS 后面被加到交错树中,因此需要先用数组存一下 
    			continue;
    		}
    		vb[to] = true;
    		if(!mth[to] or DFS(mth[to]))	{mth[to] = x; return true;}
    	}
    	return false;
    }
    
    //main 函数
    memset(la, 0xcfcf, sizeof(la));
    for(int i = 1; i <= n; i++)
    	for(int j = head[i]; j; j = edge[j].nxt)
    		la[i] = max(la[i], edge[j].wi);
    for(int i = 1; i <= n; i++){
    	while(true){//当左部点找到增广路之前 
    		memset(va, false, sizeof(va));
    		memset(vb, false, sizeof(vb));
    		memset(d, 0x3f3f, sizeof(d));
    		if(DFS(i))	break;
    		ll dlt = INF;
    		for(int j = 1; j <= n; j++) if(!vb[j]) dlt = min(dlt, d[j]);
    		for(int j = 1; j <= n; j++){
    			if(va[j])	la[j] -= dlt;
    			if(vb[j])	lb[j] += dlt;
    		}
    	}
    }
    
  2. 费用流。复杂度为 \(O(fnm)\),由于此处有 \(f \le n\),复杂度为 \(O(n^2m)\)

题目:


6. 二分图最小点覆盖

  • 结论

    二分图最小点覆盖大小 = 二分图最大匹配大小

  • 构造方法

    执行完匈牙利算法后,对所有左部非匹配点再跑一次 DFS,标记访问到的点。此时,左部未被标记的点右部被标记的点形成的就是二分图最小点覆盖。

  • 建模要素

    • 2 要素:节点可分为独立的 2 个集合,每条边至少在 2 个中选一个。
  • 题目


7. 二分图最大独立集

  • 结论

    在任意无向图中,有:最大独立集大小 = 总点数 - 最小点覆盖大小。

    故有:二分图最大独立集大小 = 总点数 - 二分图最大匹配大小。

  • 建模要素

    • 2 要素:节点可分为独立的 2 个部分,每条边最多在 2 个中选一个。
  • 题目

    • P3355 骑士共存问题:以黑/白格子为两个集合,以一次跳跃为边。(可以发现,骑士每一次跳跃会变化一次格子颜色。)

    • P5030 长脖子鹿放置:可以发现此时不能以格子颜色进行划分了。单独对行进行分析,发现每次跳跃都会改变一次行数的奇偶性。故以行奇/偶作为两个集合,以一次跳跃作为边。


8. DAG 最小路径点覆盖

  • 路径无重复

    将 DAG 的每个点拆为“出点”和“入点”,从每个出点连边指向入点。可以发现,这是一个二分图。

    可以发现,除了路径起点、终点,每个点对应的入点都有一个入度,每个点对应的出点都有一个出度。这是一组二分图匹配。

    若要使得路径尽量少,就是使得终点尽量少,即使得二分图非匹配的出点尽量少。故用 总点数 - 二分图最大匹配数 即可。

  • 路径有重复

    通过 floyd 跑传递闭包,在可达的节点之间连边,即可转化为无重复路径的情况。

  • 捉迷藏

    答案为最小路径点覆盖的数量。证明如下:

    ...

    蓝书上也给出了本题方案该如何构造:

    设答案集合为 \(E\)。初始先将所有终点加入 \(E\),然后试着调整。可以发现如果两个点之间有直接的连边就说明二者绝对可达。故此,求出 \(E\) 的可达集合 \(next(E)\),将 \(E\)\(next(E)\) 的交中的所有节点向其起点方向移动,直到其不在 \(next(E)\) 中。

    但我感觉有点问题。。。因为你向上挪动的过程中,会造成 \(next(E)\) 的扩大啊?

    举个例子:

    感觉蓝书上的构造方法有点问题啊……
    设想这么一个图:

    1 2
    1 3
    1 4
    3 5
    4 6
    5 2
    6 2
    

    然后令选出的最小点覆盖为 (1, 2),(3, 5),(4, 6)

    接着节点 2 上跳,跳到了 1
    那么请问怎么有解???

posted @ 2024-02-22 10:34  David_Mercury  阅读(13)  评论(0编辑  收藏  举报