Acwing提高课 --图论

图论

Dijkstra

时间复杂度: m l o g n mlogn mlogn

边权:非负

性质:按照dijkstra更新的点时满足拓扑序的。

dijkstra求次短路

例题:观光

int dijkstra()
{
	mem(dist, 0x3f);
	mem(st, false);
	mem(cnt, 0);
	
	dist[s][0] = 0;
	cnt[s][0] = 1;
	priority_queue<Node, vector<Node>, greater<Node>> heap;
	heap.push({0, 0, s});
	
	while(heap.size())
	{
		auto t = heap.top();
		heap.pop();
		
		int d = t.dist, type = t.type, id = t.id;
		if(st[id][type]) continue;
		st[id][type] = true;
		
		for(int i = h[id]; ~i; i = ne[i])
		{
			int j = e[i];
			if(dist[j][0] > d + w[i])
			{
				dist[j][1] = dist[j][0], cnt[j][1] = cnt[j][0];//最短被更新,次短继承最短。
				heap.push({dist[j][1], 1, j});//将更新的次短路入堆
				dist[j][0] = d + w[i], cnt[j][0] = cnt[id][type]; //更新最短路
				heap.push({dist[j][0], 0, j});//将更新的最短路入堆
			}
			else if(dist[j][0] == d + w[i])	cnt[j][0] += cnt[id][type];//找到等价最短路
			else if(dist[j][1] > d + w[i])//次短路被更新
			{
				dist[j][1] = d + w[i];
				cnt[j][1] = cnt[id][type];
				heap.push({dist[j][1], 1, j});//将更新的次短路入堆
			}
			else if(dist[j][1] == d + w[i])	cnt[j][1] += cnt[id][type];//找到等价次短路
		}
	}
	
	int ans = cnt[t][0];
	if(dist[t][0] + 1 == dist[t][1]) ans += cnt[t][1];
	return ans;
}

Spfa

时间复杂度:

边权:可正可负

边权非负还是用dijkstra保险

Floyd

时间复杂度: O ( n 3 ) O(n^3) O(n3)

思想:DP

d[k][i][j]: i i i j j j只经过 1 − k 1 - k 1k的最短路
插点更新:用k点更新之前,最短路是由1~k-1来更新的。

板子

mem(d, 0x3f, sizeof d);
rep(i, 1, n) d[i][i] = 0;

rep(k, 1, n)
	rep(i, 1, n)
		rep(j, 1, n)
			if(d[i][j] > d[i][k] + d[k][j])
               {
                   	d[i][j] = d[i][k] + d[k][j];
                 	path[i][j] = k;//记录路径
               }

Floyd 找最小环?

DP思想:

以K(环中最大点)枚举所有的环。

在这里插入图片描述

/*
author: A Fei
*/ 
#include <bits/stdc++.h>

#define fi first
#define se second
#define endl '\n'
#define ios ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define mem(x, a) memset(x, a, sizeof x)
#define pb(x) push_back(x)
#define rep(i, l, r) for(int i = l; i <= (r); ++ i)
#define per(i, r, l) for(int i = r; i >= (l); -- i)

using namespace std;
typedef long long LL;
typedef double DB;
typedef pair<int, int> PII;
typedef pair<int, double> PID;
typedef pair<double, double> PDD;

int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
const int INF = 0x3f3f3f3f;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const int N = 110;
int g[N][N], d[N][N];
int path[N][N];//保存路径。
int n, m;

vector<int> ans;

void dfs(int i, int j)//输出最短路部分的路径
{
    int k = path[i][j];
    if(!k) return ;
    
    dfs(i, k);
    ans.pb(k);
    dfs(k, j);
}

void get_path(int i, int j, int k)//记录最小环的方案:i -> 最短路部分 -> j -> k
{
    ans.clear();
    ans.pb(i);
    dfs(i, j);
    ans.pb(j);
    ans.pb(k);
}

int main()
{
    cin >> n >> m;
    
    memset(g, 0x3f, sizeof g);
    rep(i, 1, n) g[i][i] = 0;
    
    while (m -- )
    {
        int a, b, c;
        cin >> a >> b >> c;
        
        g[a][b] = g[b][a] = min(g[a][b], c);
    }
    
    memcpy(d, g, sizeof g);//将g备份到d,用d跑最短路。
    int res = INF;
    rep(k, 1, n)
    {
        //d此时为:只被1~k-1更新的最短路,来保证k为环中最大的点。
        rep(i, 1, k - 1)//i,j,k不相同来,找以K最大点的最小环。
            rep(j, i + 1, k - 1)//i < j < k
                if(res > (LL)g[i][k] + g[k][j] + d[i][j])//三个都是INF的话,这里可能会爆。。。。艹
                {
                    res = g[i][k] + g[k][j] + d[i][j];//更新最小环的权值。
                    get_path(i, j, k);//记录目前最小环的路径。
                }
        
        //Floyd更新最短路并记录路径
        rep(i, 1, n)
            rep(j, 1, n)
                if(d[i][j] > d[i][k] + d[k][j])
                {
                    d[i][j] = d[i][k] + d[k][j];
                    path[i][j] = k;
                }
    }
    
    if(res == INF) puts("No solution.");
    else    for(auto x : ans) cout << x << " ";
    
    return 0;
}

类Floyd(坑)

Floyd的倍增算法?
DP – 状态表示:d[k][i][j]: i i i j j j恰好经过k条边的最短路.

最小生成树

**1.**将所有边加入结构体,并从小到大排序。

**2.**选择两端点不在一个集合的边, 并将两端点合并.

(建图的时候可能会有离散化的操作)

次小生成树(坑)

定义给定一个带权的图,把图的所有生成树按权值从小到大排序,第二小的称为次小生成树。

步骤:
1.先构建最小生成树。
2.枚举非树边,将非树边替换至最小生成树中,同时从树中去掉一条边,使得最终的图仍是一颗树。

eg:秘密的奶牛运输
解法:dfs + 最小生成树 + 枚举非树边

负(正)环

判法一:
一个点最多被 n − 1 n-1 n1的点相连;当某一个点入队次数 > = >= >=n即被更新了多于n次。即存在负(正)环

判法二
n n n个点的图,最短路只可能存在 n − 1 n-1 n1条边,当某个点被更新时,从起点到该点的边数 > = n >=n >=n时,即存在负(正)环

一般用第二种判法
因为当 n n n个点形成一个大负(正)环,用判法一则需要转 n n n圈才能结束– O ( n 2 ) O(n^2) O(n2);判法二则需要转一圈即可 – O ( n ) O(n) O(n)

玄学优化:
1.使用会比队列效率高。
2.经验值trick-- 当所有点入队次数超过2N,我们就认为图中很大可能存在负环。

Code:

/*
author: A Fei
solution: 
			将每个点的前两个字母和后两个字母看成两个点!
			
			设ans为解 --> ans * (n) - (w[1]+w[2]+w[3]+...+w[n]) = 0
			二分mid,当mid < ans 则存在负环;当mid >= ans不存在负环。
			即存在二分性质。
*/ 
#include <bits/stdc++.h>

#define fi first
#define se second
#define endl '\n'
#define ios ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define mem(x, a) memset(x, a, sizeof x)
#define pb(x) push_back(x)
#define rep(i, l, r) for(int i = l; i <= (r); ++ i)
#define per(i, r, l) for(int i = r; i >= (l); -- i)

using namespace std;
typedef long long LL;
typedef double DB;
typedef pair<int, int> PII;
typedef pair<int, double> PID;
typedef pair<double, double> PDD;

int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
const int INF = 0x3f3f3f3f;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const int N = 700, M = 9e5 + 10;
int h[N], ne[M], e[M], w[M], idx;
double dist[N], cnt[N];
bool st[N];
int n, Cnt;

void add(int a, int b, int c)
{
    ne[idx] = h[a], e[idx] = b, w[idx] = c, h[a] = idx ++;
}

bool check(double mid)
{
    // mem(st, false);

    stack<int> q;
    rep(i, 1, Cnt) q.push(i), st[i] = true, dist[i] = 0, cnt[i] = 0;

    int sum = 0;
    while(q.size())
    {
        int t = q.top();
        q.pop();
        st[t] = false;


        for(int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];

            if(dist[j] > dist[t] + mid - w[i])
            {
                dist[j] = dist[t] + mid - w[i];
                cnt[j] = cnt[t] + 1;
                if(cnt[j] >= N) return true;
                if(++ sum >= 10000) return true;
                if(!st[j])  q.push(j), st[j] = true;
            } 
        }
    }

    return false;
}

int main()
{
    while(scanf("%d", &n), n)
    {
        mem(h, -1);
        idx = 0;

        unordered_map<string, int> id;
        Cnt = 0;
        rep(i, 1, n)
        {
            string s;
            cin >> s;

            if(s.length() <= 1) continue;
            string ss = s.substr(0, 2);
            if(!id[ss]) id[ss] = ++ Cnt;
            string ee = s.substr(s.length() - 2);
            if(!id[ee]) id[ee] = ++ Cnt;
            add(id[ss], id[ee], s.length());
            // cout << ss << " " << ee << " " << s.length() << endl;
        }

        if(!check(0)) 
        {
            puts("No solution");
            continue;
        }
        double l = 0, r = 10000, eps = 1e-3;
        while(r - l >= eps)
        {
            double mid = (l + r) / 2;

            if(check(mid)) l = mid;
            else r = mid;
        }

        printf("%.2lf\n", l);
    }
    return 0;
}

差分约束

利用图来解决:求不等式组的可行解最大最小值

求不等式组可行解:

步骤:
1.将不等式组变形为形如这样的: X i < = X j + C k X_i <= X_j + C_k Xi<=Xj+Ck
连接一条 X j X_j Xj -> X i X_i Xi权值为 C k C_k Ck的边.(即:将限制条件变为一条边)
2.建立炒鸡源点能讲遍历到所有边(所有限制条件).
3.在建立的图上面跑最短路或最长路即可得到一组可行解.

求最大最小值:

结论:
最大值最短路;求最小值最长路.

解释:
有一条边(限制条件): X j X_j Xj -> X i X_i Xi,边权为 C k C_k Ck.

当跑最短路后:
d [ i ] < = d [ j ] + C k d[i] <= d[j] + C_k d[i]<=d[j]+Ck

当跑最长路后:
d [ i ] > = d [ j ] + C k d[i] >= d[j] + C_k d[i]>=d[j]+Ck

跑完最短路后:我们求出的是每个点 i i i的上限,将求得的每个点的上限相加即得到最大值.
跑完最长路后:我们求出的是每个点 i i i的下限,将求得的没给单的下限相加即得到最小值.

:边与边的关系求出的只是每个变量之间的相对关系.要想求出最大最小值,我们需要一个形如: X y > = C X_y >= C Xy>=C(绝对关系), 并通过这个变量能得到其他变量的的绝对关系.

eg: 区间
解法一:差分约束
解法二:贪心 + 线段树优化(挖坑)

Code:

/*
author: A Fei
solution:
            艹,自己搞得前缀和,条件搞了一坨屎,还搞了个牛马离散。-- https://www.acwing.com/problem/content/submission/code_detail/13406556/
            前缀和:
                条件一:s_i >= s_i-1  add(si-1, si, 0)                  
                条件二:s_i - s_i-1 <= 1 --> s_i-1 >= s_i - 1   add(si, si-1, -1)
                条件三:s_b - s_a-1 >= c    add(sa-1, sb, c);

            求完最短路,s_50001即为答案。
*/ 
#include <bits/stdc++.h>

#define fi first
#define se second
#define endl '\n'
#define ios ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define mem(x, a) memset(x, a, sizeof x)
#define pb(x) push_back(x)
#define rep(i, l, r) for(int i = l; i <= (r); ++ i)
#define per(i, r, l) for(int i = r; i >= (l); -- i)

using namespace std;
typedef long long LL;
typedef double DB;
typedef pair<int, int> PII;
typedef pair<int, double> PID;
typedef pair<double, double> PDD;

int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
const int INF = 0x3f3f3f3f;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const int N = 5e4 + 10, M = 150001;
int h[N], ne[M], e[M], w[M], idx;
int dist[N];
bool st[N];
int n, m;

void add(int a, int b, int c)
{
    ne[idx] = h[a], e[idx] = b, w[idx] = c, h[a] = idx ++;
}

void spfa()
{
    mem(dist, -0x3f);
    queue<int> q;
    q.push(0), st[0] = true, dist[0] = 0;

    while(q.size())
    {
        int t = q.front();
        q.pop();
        st[t] = false;

        for(int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if(dist[j] < dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if(!st[j]) q.push(j), st[j] = true;
            }
        }
    }

}

int main()
{   
    scanf("%d", &n);
    mem(h, -1);

    rep(i, 1, N - 1) add(i - 1, i, 0), add(i, i - 1, -1);

    rep(i, 1, n)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        a ++, b ++;
        add(a - 1, b, c);
    }
    // exit(0);
    spfa();
    cout << dist[50001];


    return 0;
}

最近公共祖先(LCA - Least Common Ancestors)

法一: 倍增
f [ i , j ] f[i,j] f[i,j]:表示从i开始,向上走 2 j 2^j 2j步所走到的节点。
depth[i]:表示i点的深度。
步骤:

  1. 先将两个点跳到同一层。
    2.让两个点同往上跳,一直跳到它们最近公共祖先的下一层子节点。
  • O ( n l o g n + m l o g n ) O(nlogn + mlogn) O(nlogn+mlogn):预处理 O ( n l o g n ) O(nlogn) O(nlogn) + 查询 O ( m l o g n ) O(mlogn) O(mlogn)

eg:祖孙询问

LCA:可以求两个点之间的最近祖先两个点之间距离的最大,次大,最小值

Code:

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++; 
}

void bfs(int r)
{
    memset(depth, 0x3f, sizeof depth);
    queue<int> q;
    q.push(r);
    depth[r] = 1;//根节点的深度定义为1
    depth[0] = 0;//哨兵深度定义为0,设立哨兵方便处理跳过根节点的情况
    
    while(q.size())
    {
        int t = q.front();
        q.pop();
        
        for(int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            //表示未遍历过
            if(depth[j] > depth[t] + 1) 
            {
                depth[j] = depth[t] + 1;
                q.push(j);
                fa[j][0] = t;//此时j的父亲即为t
                for(int k = 1; k <= 16; k ++)//更新j的祖宗们
                    fa[j][k] = fa[fa[j][k-1]][k-1];
                /*
                    fa[j][k]:j向上2^k步的祖宗
                    fa[j][k-1]:j向上2^k-1步的祖宗,在该基础上再走2^k-1步即为fa[fa[j][k-1]][k-1]
                    所以:fa[j][k] = fa[fa[j][k-1]][k-1];
                    
                    如果向上已经超过根节点,那么祖宗节点即为哨兵0
                */
            }
        }
    }
}

int lca(int a, int b)
{
    if(depth[a] < depth[b]) swap(a, b);
    
    //先将a, b跳到同一层
    for(int i = 16; i >= 0; i --)
        if(depth[fa[a][i]] >= depth[b]) a = fa[a][i];
    /*
        如果跳过的话,那么有depth[fa[a][i]] = 0 < depth[b] 就不会跳这么远,就会向近一点的考虑
    */
    
    if(a == b) return a;
    /*
    此时两个点同时向上跳
    1.如果跳过根节点,那么fa[a][i] = fa[b][i] = 0,不会跳过去
    2.if(fa[a][i] != fa[b][i])这个限制条件保证了跳的位置一定是在最近公共点的下方,最后一定会停在最近公共点的子节点处
    */
    for(int i = 16; i >= 0; i --)
        if(fa[a][i] != fa[b][i])    a = fa[a][i], b = fa[b][i];
    
    return fa[a][0];
}

法二: Tarjan 离线LCA
时间复杂度: O ( m + n ) O(m+n) O(m+n)
个人赶脚很鸡肋,没倍增的好用,等倍增被卡了,再学。

树上差分

eg: 闇の連鎖

树上差分

有向图的强连通分量

概念:
对于一个有向图来说:
连通分量:对于分量中任意两点, u , v u, v u,v,必然可以从 u u u走到 v v v,且从 u u u走到 v v v
强连通分量(SCC-strongly connected component):极大连通分量 – 加上任意一个点都不是连通分量。

关于有向图的知识:
在这里插入图片描述
黑色:树枝边
蓝色:前向边
绿色:后向边
红色:横插边

当存在后向边横插边,才可能存在SCC

Tarjan求SCC
对每个点定义两个时间戳 – dfn[u], low[u]
u是所在强连通分量的最够点,等价与 dfn[u] == low[u]

具体Code:

const int N = 1e4 + 10, M = 5e4 + 10;
int h[N], ne[M], e[M], idx;//邻接表
//时间戳
//dfn[u]:表示遍历到u的时间戳
//low[u]:从u开始走,所能遍历到的最小时间戳
int dfn[N], low[N], timesamp;
int stk[N], top;//栈
int scc_cnt, scc_size[N], scc_id[N];//强联通块数量;每个强联通块中点的数量;强联通块的编号
bool in_stk[N];//元素是否入栈
int dout[N];//每个强联通块的出度
int n, m;

void add(int a, int b)
{
    ne[idx] = h[a], e[idx] = b, h[a] = idx ++;
}

void tarjan(int u)
{
    dfn[u] = low[u] = ++ timesamp;//给u赋予时间戳
    stk[++ top] = u, in_stk[u] = true;//将u入栈
    
    for(int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        
        if(!dfn[j])//j没遍历过
        {
            tarjan(j);//遍历j
            low[u] = min(low[j], low[u]);//回溯,更新u所能到的最小时间戳
        }
        else if(in_stk[j])//儿子已经在栈里面,即u这条边是一条后向边或横插边 --> 意味着j的时间戳是小于u的时间戳
            low[u] = min(low[u], dfn[j]);
    }
    
    //不懂这点就模拟一下:1->2, 2->3, 3->1这个图
    if(dfn[u] == low[u])//回溯的时候,发现u的时间戳和所能到的最小时间戳相等,那么从栈顶到栈中的u之间的元素就是一个SCC
    {
        ++ scc_cnt;//SCC数量+1
        int y;
        do{
            y = stk[top --];//取出栈中元素
            in_stk[y] = false;//出栈置为false
            scc_id[y] = scc_cnt;//给元素y编号
            scc_size[scc_cnt] ++;//该SCC点数量+1
        }while(y != u);
    }
}

无向图的双连通分量(坑)

二分图

二分图指无向图
1.二分图 <=> 图中不存在奇数(边)环 <=> 染色法不存在矛盾
2.匈牙利算法(求二分图最大匹配),匹配,最大匹配,匹配点,增广路径
3.最小点覆盖,最大独立集,最小路径点覆盖(最小路径重复点覆盖)

无向图中 最 大 匹 配 数 = 最 小 点 覆 盖 = 总 点 数 ( 两 边 总 点 数 ) − 最 大 独 立 集 最大匹配数 = 最小点覆盖 = 总点数 (两边总点数)- 最大独立集 ==
有向无环图: 最 大 匹 配 数 = 总 点 数 ( 一 边 总 点 数 ) − 最 小 路 径 点 覆 盖 最大匹配数 = 总点数(一边总点数)- 最小路径点覆盖 =

最小点覆盖: 给定一张无向图,每条边至少选一个点,所选点数 最少

最大独立集:给定一张无向图,选出最多的点,使得选出的点之间没有边(等价:选最少的点,将边破坏掉 即:总点数 - 最小点覆盖)。
最大团:给定一张无向图,选出最多的点,使得任意两点都有边

  • 最大独立集与最大团:互补
  • G G G最大独立集 = 补图 G 1 G_1 G1的最大团。
    G G G去掉已有的边,加上没有的边 --> 补图 G 1 G_1 G1

最小路径点覆盖有向无环图中,用最少的互不相交(点不重复)的路径,将所有点覆盖。

匈牙利算法(二分图最大匹配):
只建立有向边(从一边到另一边)即可。

eg: 简单染色
二分图最大匹配
最小点覆盖
最大独立集
最小路径点覆盖

Code(二分图最大匹配):

/*
author: A Fei
solution:
        不考虑不能放的格子,将坐标和为偶数的记为白格子,奇数的记为黑格子。
        显然白色格子周围都是黑色格子,黑色格子周围都是白色格子,当白色格子向走位黑色格子连一条有向边时即骨牌数量+1
        同色之间不可能相连,故该图为二分图。
        问题转化为求二分图的最大匹配。
*/ 
#include <bits/stdc++.h>

#define fi first
#define se second
#define endl '\n'
#define ios ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define mem(x, a) memset(x, a, sizeof x)
#define pb(x) push_back(x)
#define rep(i, l, r) for(int i = l; i <= (r); ++ i)
#define per(i, r, l) for(int i = r; i >= (l); -- i)

using namespace std;
typedef long long LL;
typedef double DB;
typedef pair<int, int> PII;
typedef pair<int, double> PID;
typedef pair<double, double> PDD;
LL read(){LL x=0,f=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}while(c>='0'&&c<='9')x=x*10+(c-'0'),c=getchar();return f*x;}

int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
const int INF = 0x3f3f3f3f;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const int N = 110;
PII match[N][N];
int n, m;
bool bad[N][N], st[N][N];

bool find(int x, int y)
{
    rep(i, 0, 3)
    {
        int a = x + dx[i], b = y + dy[i];//邻近格子
        if(a < 1 || a > n || b < 1 || b > n || bad[a][b] || st[a][b]) continue;//出界or格子不能放or格子遍历过
        
        st[a][b] = true;//妹子被遍历
        PII t = match[a][b];//t --> 妹子的对象
        if(t.fi == -1 || find(t.fi, t.se))//-1:妹子没对象 or 妹子的对象可以换个对象 则匹配成功
        {
            match[a][b] = {x, y};//将妹子的对象换为x,y
            return true;//x,y与a,b配对成功
        }
    }
    
    return false;
}

int main()
{
    scanf("%d%d", &n, &m);
    
    rep(i, 1, m)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        bad[a][b] = true;
    }
    
    mem(match, -1);
    
    int res = 0;
    //枚举一边:白色格子or黑色格子
    rep(i, 1, n)
        rep(j, 1, n)
        {
            mem(st, false);
            if(((i + j) & 1) && !bad[i][j] && find(i, j))//枚举奇数格子,能放且能分配白格子
                res ++;
        }
    
    printf("%d\n", res);
    return 0;
}

欧拉回路和欧拉路径

  1. 所有边联通的无向图

(1)存在欧拉路径的充要条件:度数位奇数的点只能有02个。
(2)存在欧拉回路的充要条件:度数位奇数的点只能有0个。

2.所有边都联通的有向图

(1)。存在欧拉路径的充要条件:

a.所有点出度均等于入度

b.除起点(入度+1=出度)和终点(入度=出度+1)外,剩余点入度=出度。

(2).存在欧拉回路的充要条件:所有点的出度=入度。

步骤
1.通过有向图/无向图的入度出度条件判断是否有欧拉路径(回路)
2.建图,遍历图
3.每遍历一条边就将其删除
4.在回溯时记录路径,将记录的路径逆序输出即为真正路径。

Code:骑马修栅栏–邻接矩阵删边

const int N = 510, M = 2100;
int g[N][N];
int n, m, cnt;
int d[N], path[N];

void dfs(int u)
{
    rep(i, 1, 500)
        if(g[u][i])
        {
            g[u][i] --, g[i][u] --;
            dfs(i);
        }
    path[++ cnt] = u;
}

int main()
{
    scanf("%d", &m);
    rep(i, 1, m)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        g[a][b] ++, g[b][a] ++;
        d[a] ++, d[b] ++;
    }
    
    int start = 1;
    while(!d[start]) start ++;
    
    rep(i, start, 500)//要从一个奇度点出发
        if(d[i] & 1) 
        {
            start = i;
            break;
        }
    dfs(start);
    per(i, cnt, 1) printf("%d\n", path[i]);
    
    return 0;
}

Code:欧拉回路–邻接表删边

/*
author: A Fei
solution: 
            1.当存在自环时O(m^2)的原因:
                大概是这么个意思,只有一个点的情况下,
                假如第一次把第一个环标记了,在遍历第二个环的时候又回到了第一个点(因为只有一个点,所以就围着点转圈),
                那么还是会枚举第一条边,虽然会接着被continue掉,
                以此类推,在枚举到第m条边的时候,还是会枚举到前边m-1条边(虽然会被continue掉),
                那么这就成了O(m2)O(m2)了,但是如果直接删掉的话就直接少了枚举第一条边了,那么复杂度就成了线性了。
                    -- tocsu的评论:https://www.acwing.com/video/592/   
            2.修改前TLE以及y总加完引用AC的原因:
                a。递归到最后一层时,h[u]=-1了,再回溯时whie(~h[u])就结束了。
                原来y总代码,虽然h[u]最后也等于-1,但是结束条件是~i, i=ne[i]还是继续遍历
                b。详细解释 - https://www.acwing.com/solution/content/38082/
            3.倒序记录边的原因:
                一个点的出边可能有多条,有些出边可能要回溯的时候才能访问到,如果遍历到的时候直接正序记录,
                那么终点/终边可能就会出现在回溯后才能访问到的点/边前面,这样记录的顺序就是错的。
                    -- jzdx的评论:https://www.acwing.com/video/592/
            
            tricks:
                a.无向图:i的反向边:h[i^1]
                b.无向图:0,1为第一条边;2,3为第二条边;4,5为第三条边...
*/ 
#include <bits/stdc++.h>
const int N = 1e5 + 10, M = 4e5 + 10;
int h[N], ne[M], e[M], idx;
bool used[M];
int cnt, path[M / 2];
int din[N], dout[N];
int type;
int n, m;

void add(int a, int b)
{
    ne[idx] = h[a], e[idx] = b, h[a] = idx ++;
}

void dfs(int u)
{
    while(h[u] != -1)
    {
        int i = h[u];
        if(used[i])//该边被用过
        {
            h[u] = ne[i];
            continue;
        }
        h[u] = ne[i];//这条边用过删除
        if(type == 1) used[i ^ 1] = true;//该边的反向也标记
        
        dfs(e[i]);
        
        int t = i;
        if(type == 1) //0, 1为第一条边;2,3为第二条边;...
        {
            t = t / 2 + 1;
            if(i & 1) t = -t;
        }
        else t ++;
        path[++ cnt] = t;
    }
}

int main()
{
    mem(h, -1);
    scanf("%d", &type);
    
    scanf("%d%d", &n, &m);
    rep(i, 1, m)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
        if(type == 1)   add(b, a);
        din[b] ++, dout[a] ++;
    }
    
    if(type == 1)//无向图
    {
        rep(i, 1, n)
            if((din[i] + dout[i]) & 1)//存在奇度点
            {
                puts("NO");
                return 0;
            }
    }
    else //有向图
    {
        rep(i, 1, n)
            if(din[i] != dout[i])//入度与出度不同
            {
                puts("NO");
                return 0;
            }
    }
    
    //找到有边的点爆搜
    rep(i, 1, n)
        if(h[i] != -1)
        {
            dfs(i);
            break;
        }
    
    //没搜到全部的边--边不连通
    if(cnt != m) 
    {
        puts("NO");
        return 0;
    }
    puts("YES");
    // cout << cnt;
    per(i, cnt, 1)
        printf("%d ", path[i]);
    
    return 0;
}

拓扑排序

a. DAG存在拓扑序
b. 存在拓扑序之后就可以DP,记忆化搜索,用Bfs跑最长最短路…

步骤:
a.统计每个点的入度
b.将入度为0的入队
c.跑图,遍历一条边就将对点入度- -,若为0,将其入队。

eg:车站分级

Code:

void topsort()
{
	rep(i, 1, n) dist[i] = 1;
	queue<int> heap;
	rep(i, 1, n + m)
		if(!din[i]) heap.push(i);//车站编号至少为1
	
	while(heap.size())
	{
		int t = heap.front();
		heap.pop();
		
		for(int i = h[t]; ~i; i = ne[i])
		{
			int j = e[i];
			dist[j] = max(dist[j], dist[t] + w[i]);
			if(-- din[j] == 0) heap.push(j);
		}
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值