zoukankan      html  css  js  c++  java
  • 图论算法总结

    #图论小结(18.8.17) ##前言 学了一星期的图论终于A穿了集训的图论专题,趁热打铁先总结一波集训出现的图论知识方便回忆

    图论基础

    图的种类

    图由顶点和边组成,分为有向图和无向图。
    通常点的符号为u和v,边用符号e表示,连接u和v两点的边记为e=(u,v)。点的集合用V表示,边的集合用E表示,以V和E表示的图记为G=(V,E)。

    无向图基础术语

    两点相邻:两个顶点之间有边连接,称这两点相邻。
    路径:相邻顶点的序列
    环(圈):起点与终点重合的路径
    连通图:任意两点有路径连接的图
    :连接点的边的数量
    树(Tree):没有环的连通图,树的边数一定为树的点数-1,即E=V-1
    森林:没有环的非连通图

    有向图基础术语

    入度:指向这个点的边的数量
    出度:由这个点指向其他点的边的数量
    DAG:有向无环图

    图的存储

    邻接矩阵:用一个二维数组edge[i][j]存图,初始化为-1,非-1表示从点i到点j有权值为edge[i][j]的边。
    领接表:用vector数组edge[i]存图,表示的是从i点指向的相邻的点
    前向星:与领接表没有本质的区别,但是速度更快。

    struct Star{
        int next, to;
    }edge[maxe];//边集合
    int head[maxn];//每个点的边集合开始的编号
    memset(head, -1, sizeof head);//初始化
    for(int i = head[u];i != -1;i = edge[i].next);//遍历容器
    int cnt = 1;
    void(int u,int v){
        edge[cnt].to = v;
        edge[cnt].next = head[u];
        head[u] = cnt++;
    }//加边
    

    head[i]数组存储的是由i点指向相邻点的边在edge的编号(即edge[head[i]]),next存的是下一条边,to是连接的点,类比链表。

    图的搜索

    dfsbfs,不懂的用百度搜索吧。

    并查集

    [并查集入门],讲的肥肠生动,强烈推荐
    [对并查集和带权并查集的深入理解],讲的肥肠详细,强烈推荐。
    感觉带权并查集就是每个点都带有属性,在进行合并操作的时候对属性进行判断就可以,和不带权没有太大的区别。

    例题:POJ-1182 食物链
    题意:中文,点链接看题
    思路:重点记录一下如何用向量思维做这题。
    建立一个结构体,包含这个点的父亲的信息和它与父亲的关系,设0为同类,1为被父亲吃,2为吃父亲,以此为权建立带权并查集。那么并查集路径压缩和合并时的关系转换可以用以下公式解决:

    路径压缩:儿子节点与根节点的关系 = (儿子节点与父亲的关系+父亲节点与根的关系)%3
    集合合并:y根节点与x根节点的关系 = (y根节点与y节点的关系 + y节点与x节点的关系 + x节点与x根节点的关系)%3
    关系判断:x与y节点的关系 = (x节点与xy根节点的关系 + xy根节点与y节点的关系)%3

    上面的公式是不是超级像向量相加的转移公式?什么你说为什么还有父亲与儿子的关系,先回答我是不是超级像向量相加的转移公式?像就好啦加个负号不就换过来了吗,这就是转态转移的向量思维。

    #include<iostream>
    #include<cstdio>
    #include<stack>
    #include<vector>
    #include<map>
    #include<set>
    #include<algorithm>
    #include<cmath>
    #include<string>
    #include<string.h>
    #include<queue>
    #include<functional>
    using namespace std;
    #define fi first
    #define se second
    #define mp make_pair
    #define pb push_back
    #define rep(i, a, b) for(int i=(a); i<(b); i++)
    #define sz(a) (int)a.size()
    #define de(a) cout<<#a<<" = "<<a<<endl
    #define dd(a) cout<<#a<<" = "<<a<<" "
    #define be begin
    #define en end
    typedef long long ll;
    typedef pair<int, int> pii;
    typedef vector<int> vi;
    const int maxn = 50000;
    struct TREE{
    	int father;
    	int relation;//0->同类,1->被吃,2->吃
    }tree[maxn+5];
    int find(int rt){
    	if(rt == tree[rt].father) return rt;
    	int trt = find(tree[rt].father);
    	tree[rt].relation = (tree[rt].relation+tree[tree[rt].father].relation)%3;
    	tree[rt].father = trt;
    	return trt;
    }
    void unite(int re,int x,int y,int dx,int dy){
    	tree[dy].father = dx;
    	tree[dy].relation = (tree[x].relation + re +(3-tree[y].relation))%3;
    }
    int main()
    {
    	//std::ios::sync_with_stdio(false);
        //std::cin.tie(0);
    	int n,k;
    	scanf("%d%d", &n, &k);
    		for(int i = 1;i <= n;i++){
    			tree[i].father = i, tree[i].relation = 0;
    		}
    		int sum = 0;
    		while(k--){
    			int d,x,y;
    			scanf("%d%d%d", &d, &x, &y);
    			if(x > n || y > n){
    				sum++;
    				continue;
    			}
    			if(d == 1){
    				int dx = find(x),dy = find(y);
    				if(dx != dy)
    					unite(d - 1,x,y,dx,dy);
    				else if(tree[x].relation != tree[y].relation)
    					sum++;
    			}
    			if(d == 2){
    				if(x == y){
    					sum++;
    					continue;
    				}
    				int dx = find(x),dy = find(y);
    				if(dx != dy)
    					unite(d - 1,x,y,dx,dy);
    				else if(((3-tree[x].relation + tree[y].relation)%3) != 1)
    					sum++;
    			}
    		}
    		printf("%d
    ", sum);
    	return 0;
    }
    

    最短路算法

    [对最短路算法的深入理解] 推荐一下学姐的博客

    Bellman-Ford算法

    对所有点进行松弛,只要有一次松弛成功那么这个点就有可能松弛别的点,因此就对每个点进行不断的松弛知道整张图不再更新为止,算法复杂度为O(nm)(点数与边数的乘积)。

    如果图中不存在负环,那么Bellman-Ford不会经过一个点两次,即最多通过m-1条边,反之则存在负环。因此Bellman-Ford可以用来判断负环。

    SPFA算法(队列优化的Bellman-Ford算法)

    Bellman-Ford算法对于每次更新都要重新遍历所有的点,但是很显然的是只要有更新就只有这个点相邻的点可能会发生变化。所以对于每次更新,只要把成功更新的点放入队列,进行松弛时取出,直到队列为空时意味着整张图不可能继续松弛。算法复杂度为O(km)(k为一个1-4的无法严格证明的数),最坏情况可退化为O(nm)。

    for(int i = 1; i <= n; i++) book[i] = 0; //初始时都不在队列中
    queue<int> que;
    que.push(1); //将结点1加入队列
    book[1] = 1; //并打标记
    
    while(!que.empty())
    {
        int cur = que.empty(); //取出队首
        que.pop(); //队首出队
        for(int i = 1; i <= n; i++) 
        {
            if(e[cur][i] != INF && dis[i] > dis[cur]+e[cur][i]) //若cur到i有边且能够松弛
            {
                dis[i] = dis[cur]+e[cur][i]; //更新dis[i]
                if(book[i] == 0) //若i不在队列中则加入队列
                {
                    que.push(i);
                    book[i] = 1;
                }
            }
        }
        book[cur] = 0;
    }
    

    Dijkstra算法

    如果图中不存在负边,那么可以对Bellman-Ford算法进行优化:
    1)找到一个最短距离已经确定的点,从它出发开始松弛相邻的点的最短距离
    2)之后1)中的最短距离的点就可以忽略了(因为它已经不再可能更新)
    怎么得到最短距离确定的点是关键,一般的Dijk的做法是从从未使用过的顶点中寻找最短距离d[i]最小的点,由于不存在负边,那么d[i]在之后的更新中一定不会变小,即这个点一定是最短距离确定的点。以上做法可以利用堆来优化处理,把所有更新过的点存入以最短距离排序的小根堆,那么会有以下性质:

    如果有点成功松弛,那么这个点可能是最短距离确定的点,则塞入队列。
    如果这个点在队列里的最短距离大于当前的最短距离,则丢弃这个点。
    由于不存在负边以及优先队列的特性,堆顶元素一定是到达这个点的最短距离已经确定的点。

    void Dijk(){
        d[1] = 0;
        priority_queue<pair<int,int>, vector<pair<int,int> >,greater<pair<int,int> > > q;//小根堆,按照greater的定义按第一个键值排序
        q.push(make_pair(0, 1));
        while(q.size()){
        	pair<int,int> p = q.top();
        	q.pop();
        	int v = p.second;
        	if(d[v] < p.first) continue;
        	vector<A>::iterator it;//用邻接表存的图
        	for(it = edge[v].begin();it != edge[v].end();it++){
        		if(l[(*it).to] < k || l[(*it).to] > k+m) continue;
        		if(d[(*it).to] > d[v]+(*it).cost){
        			d[(*it).to] = d[v] + (*it).cost;
        			q.push(make_pair(d[(*it).to], (*it).to));
        		}
        }
    }
    

    算法复杂度为O(mlogn)

    例题:POJ-1062 昂贵的聘礼
    题意:中文题,点链接
    题解:Dijk找最短路,但是要注意等级限制:设酋长等级为L,等级限制为M,你交易的对象的等级范围只能在[L-M,L+M]中选择交易对象。枚举等级范围内的等级中心跑Dijk求最短路。(话说集训的时候居然在数学专题里做到了这题图论题,差点没把我吓死)

    #include<iostream>
    #include<vector>
    #include<cstdio>
    #include<map>
    #include<set>
    #include<algorithm>
    #include<cmath>
    #include<string>
    #include<string.h>
    #include<queue>
    #include<functional>
    using namespace std;
    #define fi first
    #define se second
    #define mp make_pair
    #define pb push_back
    #define rep(i, a, b) for(int i=(a); i<(b); i++)
    #define sz(a) (int)a.size()
    #define de(a) cout<<#a<<" = "<<a<<endl
    #define dd(a) cout<<#a<<" = "<<a<<" "
    #define be begin
    #define en end
    typedef long long ll;
    typedef pair<int, int> pii;
    typedef vector<int> vi;
    const int inf = 0x3f3f3f3f;
    struct A {
    	int to, cost;
    	A(int a, int b) :to(a), cost(b) { }
    };
    int d[105];
    int l[105], v[105];
    int main()
    {
    	std::ios::sync_with_stdio(false);
    	std::cin.tie(0);
    	int m, n;
    	while (cin >> m >> n) {
    		vector<A> edge[105];
    		for (int i = 1;i <= n;i++) {
    			int x;
    			cin >> v[i] >> l[i] >> x;
    			rep(j, 0, x) {
    				int a, b;
    				cin >> a >> b;
    				edge[i].pb(A(a, b));
    			}
    		}
    		int mi = inf;
    		for (int k = l[1] - m;k <= l[1];k++) {
    			memset(d, inf, sizeof(d));
    			d[1] = 0;
    			priority_queue<pii, vector<pii>,greater<pii> > q;
    			q.push(mp(0, 1));
    			while(q.size()){
    				pii p = q.top();
    				q.pop();
    				int v = p.se;
    				if(d[v] < p.fi) continue;
    				vector<A>::iterator it;
    				for(it = edge[v].be();it != edge[v].en();it++){
    					if(l[(*it).to] < k || l[(*it).to] > k+m) continue;
    					if(d[(*it).to] > d[v]+(*it).cost){
    						d[(*it).to] = d[v] + (*it).cost;
    						q.push(mp(d[(*it).to], (*it).to));
    					}
    				}
    			}
    			for (int i = 1;i <= n;i++) {
    				mi = min(mi, d[i] + v[i]);
    			}
    		}
    		cout << mi << endl;
    	}
    	return 0;
    }
    

    Floyd-Warshall算法(两点最短路)

    我们先定义一个数组d[i][j],表示从i到j最短的路径,那么可以用dp的思路去解决两点之间的最短路问题:

    d[i][j]表示的是从i到j的最短路径
    假设k点是ij某一条可松弛的路径经过的点
    d[i][j] = min(d[i][j], d[i][k]+d[k][j])

    for(int k = 1; k <= n; k++)
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= n; j++)
                if(d[i][j] > d[i][k]+d[k][j]) 
                    d[i][j] = d[i][k]+d[k][j];
    

    这就是Floyd-Warshall算法,时间复杂度O(n^3)

    二分图匹配

    基本术语

    假设有X集合和Y集合,两个集合的某些点相邻而集合内的点不相邻形成的图称为二分图。将两两不含公共端点的二分图边集合称为二分图匹配,元素最多的集合称为最大匹配。特别的,当每个点都成功匹配,即2*匹配数=点数,则称为完美匹配
    交替路:匹配边和非匹配边交替出现的路径
    增广路:连接两个未匹配点的交替路

    匈牙利算法

    [对匈牙利算法的深度理解]

    匈牙利算法可以看做是一个寻找增广路的算法,每当找到一条增广路意味着匹配可以优化,使得匹配数更大。时间复杂度O(n^2)原理比较好懂,参考上面链接。
    二分图的一些性质和结论

    最小点覆盖

    可以连接所有边的点集合称为点覆盖,用最少的点连接所有的边称为最小点覆盖

    结论:最小点覆盖 = 最大匹配数

    证明:由于匈牙利算法实质上是寻找增广路,而最大匹配数意味着这张匹配图里已经不存在增广路了(否则就不是最大匹配数了啊),也就是说图中已经不存在两个未匹配的点连接形成的路径了,也就说明所有的边都有点连接,即证明最大匹配数为最小点覆盖。
    例题:POJ-3041 Asteroids
    题意:消灭星星,一次可以消灭一行或一列,求最少的子弹数
    题解:把行数作为x集合,把列数作为y集合,如果一个点上有星星就把对应行列编号连线,消灭星星的最少次数就是最小点覆盖。

    #include<iostream>
    #include<stack>
    #include<cstdio>
    #include<vector>
    #include<map>
    #include<set>
    #include<algorithm>
    #include<cmath>
    #include<string>
    #include<string.h>
    #include<queue>
    #include<functional>
    using namespace std;
    #define fi first
    #define se second
    #define mp make_pair
    #define pb push_back
    #define rep(i, a, b) for(int i=(a); i<(b); i++)
    #define sz(a) (int)a.size()
    #define de(a) cout<<#a<<" = "<<a<<endl
    #define dd(a) cout<<#a<<" = "<<a<<" "
    #define be begin
    #define en end
    typedef long long ll;
    typedef pair<int, int> pii;
    typedef vector<int> vi;
    const int maxk = 10000;
    const int maxn = 500;
    struct Star{
    	int next, to;
    }edge[maxk+5];
    int head[maxn+5],flag[maxn+5],metch[maxn+5];
    bool find(int u){
    	for(int i = head[u];i != -1;i = edge[i].next){
    		int to = edge[i].to;
    		if(!flag[to]){
    			flag[to] = true;
    			if(metch[to] == 0 || find(metch[to])){
    				metch[to] = u;
    				return true;
    			}
    		}
    	}
    	return false;
    }
    int hungary(int n){
    	int sum = 0;
    	for(int i = 1;i <= n;i++){
    		if(find(i))
    			sum++;
    		memset(flag, 0, sizeof flag);
    	}
    	return sum;
    }
    int main()
    {
    	//std::ios::sync_with_stdio(false);
        //std::cin.tie(0);
    	int n,k;
    	scanf("%d%d", &n, &k);
    	memset(head, -1, sizeof head);
    	for(int i = 1;i <= k;i++){
    		int x,y;
    		scanf("%d%d", &x, &y);
    		edge[i].to = y;
    		edge[i].next = head[x];
    		head[x] = i;
    	}
    	int sum = hungary(n);
    	printf("%d
    ", sum);
    	return 0;
    }
    

    例题:POJ-2226 Muddy Fields
    题意:可以用任意长的木板覆盖泥地,只能横着或竖着铺,可以重叠,问最少木板数
    题解:假设只用横木板铺,记录木板编号(连续泥地算一块编号)加入X集合;假设只用竖木板铺,同理记录木板编号加入Y集合,每个点对应的横木板编号与竖木板编号连线,最小点覆盖就是所求。

    #include<cstdio>
    #include<iostream>
    #include<stack>
    #include<vector>
    #include<map>
    #include<set>
    #include<algorithm>
    #include<cmath>
    #include<string>
    #include<string.h>
    #include<queue>
    #include<functional>
    using namespace std;
    #define fi first
    #define se second
    #define mp make_pair
    #define pb push_back
    #define rep(i, a, b) for(int i=(a); i<(b); i++)
    #define sz(a) (int)a.size()
    #define de(a) cout<<#a<<" = "<<a<<endl
    #define dd(a) cout<<#a<<" = "<<a<<" "
    #define be begin
    #define en end
    typedef long long ll;
    typedef pair<int, int> pii;
    typedef vector<int> vi;
    const int maxn = 500;
    struct Star{
    	int next, to;
    }edge[10000];
    int head[10000];
    char maps[maxn+5][maxn+5];
    int mapx[maxn+5][maxn+5],mapy[maxn+5][maxn+5];
    bool flag[10000];
    int match[10000];
    bool find(int u){
    	for(int i = head[u];i != -1;i = edge[i].next){
    		int to = edge[i].to;
    		if(!flag[to]){
    			flag[to] = true;
    			if(match[to] == 0 || find(match[to])){
    				match[to] = u;
    				return true;
    			}
    		}
    	}
    	return false;
    }
    int hungary(int n){
    	int sum = 0;
    	for(int i = 1;i <= n;i++){
    		if(find(i))
    			sum++;
    		memset(flag, 0 ,sizeof flag);
    	}
    	return sum;
    }
    int main()
    {
    	//std::ios::sync_with_stdio(false);
        //std::cin.tie(0);
    	int r,c;
    	scanf("%d%d", &r, &c);
    	for(int i = 0;i < r;i++)
    		scanf("%s", maps[i]);
    	int cnt = 0;
    	bool f = false;
    	int n = 0;
    	for(int i = 0;i < r;i++){
    		for(int j = 0;j < c;j++){
    			if(f && maps[i][j] == '.')f = false;
    			if(maps[i][j] == '*'){
    				if(f == false)++cnt;
    				f = true;
    				mapx[i][j] = cnt;
    				n = cnt;		
    			}
    		}
    		f = false;
    	}
    	cnt = 1;
    	for(int j = 0;j < c;j++){
    		for(int i = 0;i < r;i++){
    			if(f && maps[i][j] == '.') f = false;
    			if(maps[i][j] == '*'){
    				if(f == false)++cnt;
    				f = true;
    				mapy[i][j] = cnt;
    			}
    		}
    		f = false;
    	}
    	cnt = 1;
    	memset(head, -1, sizeof head);
    	for(int i = 0;i < r;i++)
    		for(int j = 0;j < c;j++){
    			if(maps[i][j] == '*'){
    				edge[cnt].to = mapy[i][j];
    				edge[cnt].next  = head[mapx[i][j]];
    				head[mapx[i][j]] = cnt++;
    			}
    		}
    	printf("%d
    ", hungary(n));
    	return 0;
    }
    

    最小边覆盖

    可以把所有点连接起来的边集合称为边覆盖,用最少的边连接所有的点称为最小边覆盖

    结论:最小边覆盖 = 顶点数 - 最大匹配数

    证明:记点数为n,最大匹配数为m,除去得到匹配的点后剩余的点数为a。则2m+a=n,最小边覆盖=m+a。故n-m=最小边覆盖。

    DAG最小路径覆盖

    对DAG最小路径覆盖的深入理解

    最小路径覆盖分为两个,基本思路是将所有的点加入二分图的X、Y集合

    不可相交最小路径覆盖

    假设A->B,则X集合的A元素连接Y集合的B元素,连线完后即可求最大匹配数,然后套结论:

    结论:不可相交最小路径覆盖 = 原图的顶点数(即二分图顶点数/2) - 最大匹配数

    证明:一开始每个点都是独立的为一条路径,总共有n条不相交路径。我们每次在二分图里找一条匹配边就相当于把两条路径合成了一条路径,也就相当于路径数减少了1。所以找到了几条匹配边,路径数就减少了多少。所以有最小路径覆盖=原图的结点数-新图的最大匹配数。

    可相交最小路径覆盖

    假设A->C->B,直接将X集合的A元素与C、B元素连接,C也连接B,然后转化为不可相交路径覆盖问题。思路简单,证明就是A可以到B,不管有没有相交。

    例题:POJ-1422 Air Raid
    题意:在一张DAG中投放伞兵,确保每个伞兵不相交的经过所有点,问最少的伞兵数量
    题解:最小路径覆盖板子题

    #include<iostream>
    #include<cstdio>
    #include<stack>
    #include<vector>
    #include<map>
    #include<set>
    #include<algorithm>
    #include<cmath>
    #include<string>
    #include<string.h>
    #include<queue>
    #include<functional>
    using namespace std;
    #define fi first
    #define se second
    #define mp make_pair
    #define pb push_back
    #define rep(i, a, b) for(int i=(a); i<(b); i++)
    #define sz(a) (int)a.size()
    #define de(a) cout<<#a<<" = "<<a<<endl
    #define dd(a) cout<<#a<<" = "<<a<<" "
    #define be begin
    #define en end
    typedef long long ll;
    typedef pair<int, int> pii;
    typedef vector<int> vi;
    struct Star{
    	int next, to;
    }edge[100000];
    int head[200],match[200];
    bool flag[200];
    bool find(int u){
    	for(int i = head[u];i != -1;i = edge[i].next){
    		int to = edge[i].to;
    		if(!flag[to]){
    			flag[to] = true;
    			if(match[to] == 0 || find(match[to])){
    				match[to] = u;
    				return true;
    			}
    		}
    	}
    	return false;
    }
    int hungary(int n){
    	int sum = 0;
    	memset(match, 0, sizeof match);
    	for(int i = 1;i <= n;i++){
    		if(find(i))
    			sum++;
    		memset(flag, 0, sizeof flag);
    	}
    	return sum;
    }
    int main()
    {
    	//std::ios::sync_with_stdio(false);
        //std::cin.tie(0);
    	int T;
    	scanf("%d", &T);
    	while(T--){
    		int n,m;
    		scanf("%d%d", &n, &m);
    		memset(head, -1, sizeof head);
    		for(int i = 1;i <= m;i++){
    			int x,y;
    			scanf("%d%d", &x, &y);
    			edge[i].to = y;
    			edge[i].next = head[x];
    			head[x] = i;
    		}
    		printf("%d
    ", n - hungary(n));
    	}
    	return 0;
    }
    

    最大独立集

    两两不相邻的点集合称为独立集,最大的独立集称为最大独立集

    结论:最大独立集 = 顶点数 - 最大匹配数

    证明:没有匹配成功的点肯定两两不相交啊,相交了不就匹配成功了。

    LCA(最近公共祖先)

    对LCA算法的深入理解

    给定节点u、v,求距离u、v最近的公共节点即为LCA问题。求解LCA问题有三种算法:倍增LCA、ST表LCA、Tarjan_LCA(离线算法),倍增LCA时间复杂度为(O(nlogn+qlogn)),感觉没有另外两个在时间上的优势,实现和理解又有点复杂,暂时还没了解。下面记录一下另外两个算法。
    ST表LCA
    先对树从根节点进行前序遍历,记录dfs节点时经过的节点顺序,但是我们这里和dfs序有所不同,当节点回溯回父亲节点时,父亲节点还要被记录一次,我们称这种顺序为欧拉序。欧拉序可以在dfs的时候确定。

    比如上图节点的欧拉序为

    8, 5, 9, 5, 8, 4, 6, 15, 6, 7, 6, 4, 10, 11, 10, 16, 3, 16, 12, 16, 10, 2, 10, 4, 8, 1, 14, 1, 13, 1, 8

    但是这样排序不利于我们下一步的维护,我们再稍微改一下,按照第一次遍历到的编号对节点重新编号,比如上图重新编号后的欧拉序为

    1, 2, 3, 2, 1, 4, 5, 6, 5, 7, 5, 4, 8, 9, 8, 10, 11, 10, 12, 10, 8, 13, 8, 4, 1, 14, 15, 14, 16, 14, 1

    在dfs的同时,记录一下每个节点在欧拉序中出现的下标位置limit[i],记录结束以后就可以来看看欧拉序的神通了:
    假设询问节点12和11的LCA,我们在欧拉序中找到对应节点在这张欧拉序中出现的第一个位置limit[12]、limit[11],即19和14,在重新编号的新表中的对应区间[14,19]之间的最小值为8,8这个编号对应的节点是10,所以结论就是节点10是他们的LCA。
    原理很好理解,根据dfs的特性:

    编号从根节点开始,最早遍历到的节点编号一定小于后遍历到的,即子节点的编号一定比父节点的编号大
    在查找右子树之前,一定已经查完左子树并且回溯至LCA开始查右子树
    dfs没有结束不会去遍历父节点子树以外的节点

    根据这两个特性结合欧拉序的定义就不难理解:

    欧拉序里第一次出现两个节点的位置一定是第一次遍历到这个点时被记录的;
    第二个节点第一次被记录时一定会回溯经过LCA;
    LCA的编号一定是比它的子树中的任何节点的编号小的;
    dfs的第三特性保证区间内不会出现比LCA小的数。

    因此欧拉序区间内最小的编号对应的点一定是LCA。
    ST表的作用就是维护欧拉序的区间最小值,预处理时间复杂度(O(nlogn))查询时间只需要(O(1)),总时间复杂度(O(nlogn+q))

    例题:POJ-1330 Nearest Common Ancestors
    题意:给你一棵树,问两个节点的LCA
    题解:LCA板子题,每棵树的查询都只有一次,所以暴力并查集也能做

    #include<iostream>
    #include<cstdio>
    #include<stack>
    #include<vector>
    #include<map>
    #include<set>
    #include<algorithm>
    #include<cmath>
    #include<string>
    #include<string.h>
    #include<queue>
    #include<functional>
    using namespace std;
    #define fi first
    #define se second
    #define mp make_pair
    #define pb push_back
    #define rep(i, a, b) for(int i=(a); i<(b); i++)
    #define sz(a) (int)a.size()
    #define de(a) cout<<#a<<" = "<<a<<endl
    #define dd(a) cout<<#a<<" = "<<a<<" "
    #define be begin
    #define en end
    typedef long long ll;
    typedef pair<int, int> pii;
    typedef vector<int> vi;
    const int maxn = 10000;
    const double eps = 1e-9;
    struct Star{
    	int next,to;
    }edge[maxn+5];
    int head[maxn+5];
    int d[maxn<<2][64],limit[maxn+5],number[maxn+5];
    //d是ST表,limit是每个节点在欧拉序中第一次出现的下标,number记录的是重新编号对应的节点
    int cnt = 0,num = 0;
    void dfs(int u){
    	d[++cnt][0] = ++num;
    	int tnum = num;
    	limit[u] = cnt;
    	number[num] = u;
    	for(int i = head[u];i != -1;i = edge[i].next){
    		dfs(edge[i].to);
    		d[++cnt][0] = tnum;
    	}
    	return;
    }
    void makst(){
    	for(int j = 1;(1<<j) <= cnt;j++)
    		for(int i = 1;(i + (j << 1) - 1) <= cnt;i++)
    			d[i][j] = min(d[i][j-1], d[i + (1<<(j-1))][j-1]);
    }
    int query(int l,int r){
    	int k = log2(r-l+1)+eps;
    	return min(d[l][k], d[r-(1<<k)+1][k]);
    }
    bool flag[maxn+5];
    int main()
    {
    	//std::ios::sync_with_stdio(false);
        //std::cin.tie(0);
    	int T;
    	scanf("%d", &T);
    	while(T--){
    		int n;
    		scanf("%d", &n);
    		cnt = 0,num = 0;
    		memset(flag, 0, sizeof flag);
    		memset(head, -1, sizeof head);
    		for(int i = 1;i < n;i++){
    			int x,y;
    			scanf("%d%d", &x, &y);
    			flag[y] = true;
    			edge[++cnt].to = y;
    			edge[cnt].next = head[x];
    			head[x] = cnt;
    		}
    		cnt = 0;
    		for(int i = 1;i <= n;i++){
    			if(flag[i] == 0){
    				dfs(i);
    				break;
    			}
    		}
    		makst();
    		int x,y;
    		scanf("%d%d", &x, &y);
    		if(limit[x] > limit[y]) swap(x,y);
    		printf("%d
    ", number[query(limit[x],limit[y])]);
    	}
    	return 0;
    }
    

    Tarjan_LCA
    Tarjan_LCA算法在搜索的过程中有使用路径压缩,会破坏原来的树形结构,所以属于离线算法,需要把所有的询问全部读取后进行搜索。
    先将所有询问复制一份挂载在相关的节点上,比如询问[1,4],则在1节点上用队列保存询问节点4,并在4节点上用队列保存询问节点1。把所有的询问全部挂载在树上后,就成功离线了。
    搜索过程从根节点开始,当搜索到节点1时,由于节点4暂时没有被搜索,所以先标记节点1表示已被搜索并跳过这个询问。当搜索另一条子链搜索到节点4时,利用并查集的查找思想递归查找父亲并路径压缩,由于1节点已经被搜索且回溯,则当前1节点路径压缩后的父亲一定是[1,4]的LCA。Tarjan_LCA相当于dfs扫一遍整棵树的同时计算答案,时间复杂度(O(n+q))

    具体的过程推荐这篇博客对Tarjan_LCA的深入理解,讲得很好。

    例题:POJ-3728 The merchant
    题意:有一些城市相互连接,城市连接呈树状(即两个城市之间的道路有且只有一条)。有一个商人从一座城市到达另一座城市,在路上进货一次和卖出一次,问最大收益是多少
    题解:很有意思的一道题目。我们假设商人从城市A到城市B的过程看做“上山”和“下山”的过程,LCA就是这座“山”的“山峰”,那么商人可以在这些情况下获得收益

    上山时进货,上山时售货
    上山时进货,下山时售货
    下山时进货,下山时售货

    我们建立四个数组,up[x]表示从x出发上山过程中的最大收益(即第一种情况的最大收益),mi[x]表示从x出发上山过程中最低的进货价格,mx[x]表示下山到达y点过程中最高的售货价格,mx[y]-mi[x]表示从x点出发走到y点的最高收益(即第二种情况的最大收益),down[y]表示下山过程中到达y点的最大收益(即第三种情况的最大收益),所以我们就这么写更新公式

    up[x] = max(mx[y]-mi[x],max(up[x],up[y]));
    down[x] = max(mx[x]-mi[y], max(down[x],down[y]));
    mx[x] = max(mx[x],mx[y]);
    mi[x] = min(mi[x],mi[y]);

    #include<iostream>
    #include<cstdio>
    #include<stack>
    #include<vector>
    #include<map>
    #include<set>
    #include<algorithm>
    #include<cmath>
    #include<string>
    #include<string.h>
    #include<queue>
    #include<functional>
    using namespace std;
    #define fi first
    #define se second
    #define mp make_pair
    #define pb push_back
    #define rep(i, a, b) for(int i=(a); i<(b); i++)
    #define sz(a) (int)a.size()
    #define de(a) cout<<#a<<" = "<<a<<endl
    #define dd(a) cout<<#a<<" = "<<a<<" "
    #define be begin
    #define en end
    typedef long long ll;
    typedef pair<int, int> pii;
    typedef vector<int> vi;
    const int maxn = 50000;
    struct Star{
    	int next,to;
    }edge[maxn<<2];
    int head[maxn+5],cnt = 0;
    int f[maxn+5],query[maxn+5];
    struct Task{
    	int b,e,id;
    	Task(int a,int be,int c):b(a),e(be),id(c) { }
    };
    vector<Task> task[maxn+5];
    vector<pair<int,int> > v[maxn+5];
    int mx[maxn+5],mi[maxn+5],up[maxn+5],down[maxn+5];
    int find(int x){//递归回溯并更新4个数组(类似并查集的路径压缩过程)
    	if(f[x] == x) return x;
    	int y = f[x];
    	f[x] = find(y);
    	up[x] = max(mx[y]-mi[x],max(up[x],up[y]));
    	down[x] = max(mx[x]-mi[y], max(down[x],down[y]));
    	mx[x] = max(mx[x],mx[y]);
    	mi[x] = min(mi[x],mi[y]);
    	return f[x];
    }
    bool flag[maxn+5];
    void lca_tarjan(int u){
    	flag[u] = true;
    	vector<pair<int,int> >::iterator it = v[u].be();
    	for(;it != v[u].en();it++){
    		if(flag[(*it).fi]){//目标节点已被访问,由于存在后继节点,先挂载任务
    			int t = find((*it).fi);
    			if((*it).se > 0)//这题不同于普通的找LCA,询问存在方向,保证上山方向和下山方向没有搞错
    				task[t].pb(Task((*it).fi, u, (*it).se));
    			else{
    				task[t].pb(Task(u, (*it).fi, -(*it).se));
    			}
    		}
    	}
    	for(int i = head[u];i != -1;i = edge[i].next){//先访问后继节点,保证树形结构
    		if(!flag[edge[i].to]){
    			lca_tarjan(edge[i].to);
    			f[edge[i].to] = u;//由于可能存在当前节点u为子节点LCA的情况,在子节点没有搜索完成前不能指向父亲,否则路径压缩会造成树形结构破坏以及寻找LCA失败
    		}
    	}
    	vector<Task>::iterator at = task[u].be();
    	for(;at != task[u].en();at++){//回溯后处理任务
    		int x = at->b,y = at->e,id = at->id;
    		find(x),find(y);//寻找LCA
    		query[id] = max(mx[y]-mi[x],max(up[x],down[y]));
    	}
    }
    int main()
    {
    	//std::ios::sync_with_stdio(false);
        //std::cin.tie(0);
    	int n;
    	scanf("%d", &n);
    	memset(head, -1, sizeof head);
    	for(int i = 1;i <= n;i++){
    		int w;
    		scanf("%d", &w);
    		f[i] = i;
    		mi[i] = mx[i] = w;
    	}
    	for(int i = 1;i < n;i++){
    		int x,y;
    		scanf("%d%d", &x, &y);
    		edge[++cnt].to = y;
    		edge[cnt].next = head[x];
    		head[x] = cnt;
    		edge[++cnt].to = x;
    		edge[cnt].next = head[y];
    		head[y] = cnt;
    	}
    	int q;
    	scanf("%d", &q);
    	for(int i = 1;i <= q;i++){//离线询问
    		int x,y;
    		scanf("%d%d", &x, &y);
    		v[x].pb(mp(y,-i));
    		v[y].pb(mp(x,i));
    	}
    	lca_tarjan(1);
    	for(int i = 1;i <= q;i++)
    		printf("%d
    ", query[i]);
    	return 0;
    }
    

    后记

    图论还没有完全学完,等待着我的还有网络流和生成树,等我学清楚了再来总结吧

    To be continue

  • 相关阅读:
    springmvc下的web.xml的配置
    Java利用Xstream注解生成和解析xml
    第十二章 多态性与抽象类
    第十一章 继承与派生 学习笔记
    车辆选择(继承)
    5-3 两点间距离计算
    5-2 时间模拟
    5-5 多边形周长计算(继承)
    4-5 求自定类型元素的最大值 (10分)
    4-4 求自定类型元素的平均 (10分)
  • 原文地址:https://www.cnblogs.com/Ace-Monster/p/9439557.html
Copyright © 2011-2022 走看看