「ACM/OI」【图论】合集 Update: 2024.01.18

图论:


Update Information:
1.18 树
1.17 DAG 拓扑排序
1.14 BFS(图论)01bfs
1.12 :最短路和差分约束

图的存储

一、邻接表

const int maxn = 10005;  // 点数的上限
struct Edge {            // 边信息结构
  int v, w;
  Edge(){}
  Edge(int v, int w):v(v), w(w){}
};

vector<Edge> G[maxn];    //开maxn个vector,G[i]是第i个vector,用于存储第i个点的邻接边。

void init(MAXN) {        //多组测试数据需要先清空上一组的图才能使用!
  for(int i = 0; i < MAXN; i++) G[i].clear();
}
void add(int u, int v, int w) {
  G[u].push_back(Edge(v, w));
}

int main() {
  int n, m;              // n个点,m条边
  cin >> n >> m;
  // init()              // 多组测试数据需使用
  for(int i = 0, u, v, w; i < m; i++) {
    cin >> u >> v >> w;  // 每条边的起点、终点、边权
    add(u, v, w);        // 添加一条从u到v的边。
    add(v, u, w);        // 对于双向边,再添加一条反向的边。
  }
  // 建图完成

  // 遍历点u的邻居。
  for(int i = 0; i < G[u].size(); i++) {
    //注意此处G[i].size()返回一个unsigned int, 与i类型不一致,会报warning,不管他
    int v = G[u][i].v;  // 获取此边对应邻居点编号。
    int w = G[u][i].w;  // 获取此边的权值。
  }
  for(auto ed : G[u]) {
  	int v = ed.v;
  	int w = ed.w;
  }
  return 0;
}

二、链式前向星

  • 链式前向星,本质依然是邻接表,与邻接矩阵有本质区别。它是邻接表的结构使用“静态链表”的实现。所谓“静态链表”,是一种将内存先申请到程序空间内,再由程序自己控制分配的一种链表实现方式。

  • 链式前向星由三个部分组成:内存池内存池顶标(cnt)、链表头数组(head)。

  • 链式前向星所存储的信息依旧是邻接表中的边结构信息,不同之处在于将多出一个用于链表操作的字段(next,为防止部分编译器冲突,一般命名为nxt)的实现。

  • 1.边结构及内存池

struct Edge {
	int v,nxt;
	ll w;
	Edge(){}
	Edge(int v,ll w,int nxt):v(v),w(w),nxt(nxt){}
}G[maxm << 1];  //定义结构的同时声明一个足够长的数组,用于存储边信息。 
// maxm 为边数,对于双向边要2倍空间。 
  • 2.顶标和头数组的初始化

int cnt,head[maxn];
void init() {
	cnt = 0;
	memset(head,-1,sizeof(head));
}
  • 3.添加边

void add(int u, int v, ll w) {
    // 将边存入顶标位置,与u的前一条边形成链表结构,其nxt指向前一条边。
    G[cnt] = Edge(v, w, head[u]);
    head[u] = cnt++;// 将u的头记录(最新一条边)更新为最新的边。并移动顶标cnt。
}

  • 4.遍历u的所有邻居

// 先获取x的最后一条边的位置; -1表示链表结尾; 每次移动至前一条边位置
for(int i = head[u];i != -1;i = G[i].nxt) { // G[i]为当前的边
	int v = G[i].v;
	ll w = G[i].w;
}
  • ps:具体应用,详见其他模块的例题

戳我回目录


有向无环图 DAG

例题:

题目概述:有 1-N 个大写字母,且从 A 开始依次 N 个。再给定 M 个小于关系,如 A < B ,让你判断三种可能:

  • 1.在第 i 个关系给出后,是否可以满足这 N 个字母的递增关系
  • 2.在第 i 个关系给出后,是否会出现矛盾,例如之前有 A < B ,在第 i 个以后出现了 B < A
  • 3.如果 M 个关系给出后,没有出现矛盾,但无法确定 N 个字母的排列顺序,则输出  Sorted sequence cannot be determined.

在前两种情况中,输出最先满足的 i ,也就是说,按 m 个状态的顺序,满足任意一个条件后,其他条件都不用再判断。

 

题目分析:

  • 首先我们从小的向大建一条有向边
//本题不支持万能头
#include<iostream>
#include<cstring>
#include<string>
#include<algorithm>
#include<queue>
#include<cstdio>
using namespace std;

const int maxn = 1e5+5;
const int maxm = 1e6+5;

char s[100005][5];
int din[30];
int G[30][30];
vector<int> v[30];
queue<int> Q;

int n,m;
string str;

int Topo() {
    memset(din,0,sizeof(din));
    while(!Q.empty()) Q.pop();
     for(int i=0;i<26;i++) {
        for(int j=0;j<26;j++) {
            if(G[i][j] == 1) din[j]++;
        }
    }

    for(int i=0;i<n;i++) if(din[i] == 0) Q.push(i); 

    int flag = 0;
    if(Q.size() == 0) return -1;
    if(Q.size() > 1) flag = 1;
    int cnt = Q.size();
    //cout << cnt << endl;
    str = "";
    while(!Q.empty()) {
        int t = Q.front();Q.pop();
        str += t + 'A';
        int inq = 0;
        //cout << G[0][2] << endl;
        for(int i=0;i<26;i++) {

            if(G[t][i] == 0) continue;
            if(t == i) continue;
            //cout << i << endl;
            din[i]--;
            if(din[i] == 0) {
                //cout << i << endl;
                Q.push(i);
                inq++;
                cnt++;
            }
        }
        if(inq > 1) flag = 1;
    }
    //cout << cnt << endl;
    if(cnt != n) return -1;
    if(flag == 1) return 0;
    return 1;
}

int main() {
    while(scanf("%d %d",&n,&m) != EOF && n + m != 0) {//本题卡读入!!!
        memset(G,0,sizeof(G));
        string anss;
        for(int i=1;i<=m;i++) scanf("%s",s[i]);
        int ans = 0,id = 0;
        for(int i=1;i<=m;i++) {
            //v[s[i][0]-'A']
            G[s[i][0]-'A'][s[i][2]-'A'] = 1;

            int p = Topo();
            //cout << p << endl;
            if(p == -1) {
                ans = -1;
                id = i;
                break;
            }
            else if(p == 1 && id == 0) {
                ans = 1;
                anss = str;
                id = i;
                break;
            }
        }

        if(ans == -1) cout << "Inconsistency found after " << id << " relations." << endl;
        else if(ans == 1) cout <<  "Sorted sequence determined after " << id << " relations: " << anss << "." << endl;
        else cout << "Sorted sequence cannot be determined." << endl; 
    } 
    return 0;
}

例2:有向图判环

题目概述:

题目分析:

代码:


例3:Travel

拓扑方法

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 100005;//1e5+5
const int MAXM = 1000005;//1e6+5
const int INT_INF = 0x3f3f3f3f;
const ll  LL_INF = 0x3f3f3f3f3f3f3f3f;

vector<int> G[MAXN];
queue<int> Q;
int n,m;
int In[MAXN],path[MAXN];

void topol() {
    for(int i=1;i<=n;i++) {
        if(In[i] == 0) Q.push(i);
    }
    while(!Q.empty()) {
        int t = Q.front();
        Q.pop();
        for(int i=0;i<G[t].size();i++) {
            int v = G[t][i];
            path[v] = max(path[v],path[t]+1);
            In[v]--;
            if(In[v] == 0) Q.push(v);
        }
    }
}

int main() {
    scanf("%d %d",&n,&m);
    for(int i=1;i<=m;i++) {
        int u,v;
        scanf("%d %d",&u,&v);
        G[u].push_back(v);
        In[v]++;
    }
    for(int i=1;i<=n;i++) path[i] = 1;
    topol();
    int Max = -INT_INF;
    for(int i=1;i<=n;i++) if(Max < path[i]) Max = path[i];
    printf("%d\n",Max);
    return 0;
}

戳我回目录


BFS(图论)

树 / 图上的BFS

BFS序列

一般图上的BFS

双端队列BFS

适用范围

边权值为可能有,也可能没有(由于 BFS 适用于权值为 1 的图,所以一般权值是 0 或 1),或者能够转化为这种边权值的最短路问题。双端队列 BFS 又称 0-1 BFS。

过程

  1. 起点入队
  2. 队列非空时,取队首元素,用于更新其他节点
  3. 若可以更新:① 边权为0扩展到的点,放到队首(pop_front) ②有边权的边扩展到的点,放到队尾(pop_back) 。这样即可保证像普通BFS一样真个队列,队首到队尾权值单调非降
  • 基于边权01特征的最短路求解,相比于 Dijkstra堆优化的 O ( m l o g n ) O(mlogn) O(mlogn)(使用优先队列的 l o g n logn logn 来维护),双端队列的使用使得队列维护的复杂度降到了常数级 O ( 1 ) O(1) O(1),从而整体的 01BFS 复杂度降到了 O ( m ) O(m) O(m)

例题1:洛谷P4554 小明的游戏

题目描述:
一个棋盘,从起点点走到终点,上下左右。棋盘坐标中有两种符号,符号相同不花费,符号不相同花费1,求从起点到终点的最小花费。
典型的边权为0和1

代码
#include<bits/stdc++.h>
using namespace std;

#define mem(a, b) memset(a, b, sizeof(a))
typedef long long ll;

const int inf = 0x3f3f3f3f;
const int maxn = 3e4+5;
const int maxm = 1e4+5;

string s[505];
int n, m;

int dir[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};
int dis[505][505];

void solve() {
	//if(n == 0 && m == 0) return;
	mem(dis, 0x3f);
	for(int i=0;i<n;i++) cin >> s[i];
	int X1,Y1,X2,Y2; cin >> X1 >> Y1 >> X2 >> Y2;
	deque<pair<int,int> > q;
	map<pair<int,int>, int> vis;
	
	q.push_front({X1,Y1});
	dis[X1][Y1] = 0;
	
	while(!q.empty()) {
		auto t = q.front(); q.pop_front();
		int x = t.first;
		int y = t.second;
		//cout << x << " " << y << endl; 
		if(vis[{x,y}]) continue;
		vis[{x,y}] = 1;
		
		for(int i=0;i<4;i++) {
			int tx = x + dir[i][0];
			int ty = y + dir[i][1];
			if(tx < 0 || tx > n-1 || ty < 0 || ty > m-1) continue;
			int w = (s[x][y] != s[tx][ty]); // 边权0 or 1
			
			if(dis[tx][ty] > dis[x][y] + w) {
				dis[tx][ty] = dis[x][y] + w;
				if(w) q.push_back({tx,ty});
				else q.push_front({tx,ty});
			}
		}
	}
	cout << dis[X2][Y2] << "\n";
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	while(cin >> n >> m && n && m) {
		solve();
	}
	return 0;
}  

例题2:CF1063B Labyrinth

题目描述:
从一个起点走迷宫,规定只能向左走 l l l 步,向右走 r r r 步,向上和向下任意走,求能够到达的位置的个数。

分析
  • 01bfs 并不仅仅局限于01边权,对于本题,向上和向下走没有消耗,能走就多走,相当于0,故push_front;而向左和向右有消耗,故push_back,双端队列中存储坐标和剩余可走的 l l l r r r
代码
#include<bits/stdc++.h>
using namespace std;

int dir[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};  
int vis[2005][2005];
int dis[2005][2005];

int ans;
string s[2005];

struct Node {
	int x,y;
	int rl,rr;
	Node(){}
	Node(int x, int y, int rl, int rr):x(x),y(y),rl(rl),rr(rr){}
};

void solve() {
	int n,m; cin >> n >> m;
	int stx, sty; cin >> stx >> sty;
	stx--; sty--;
	int l,r; cin >> l >> r;
	for(int i=0;i<n;i++) cin >> s[i];
	
	deque<Node> q;
	q.push_front(Node(stx,sty,l,r));
	dis[stx][sty] = 0;
	vis[stx][sty] = 1;
	ans ++;
	
	while(!q.empty()) {
		auto pos = q.front(); 
		q.pop_front();
		//cout << pos.x+1 << " " << pos.y+1 << " l,r " << pos.rl << " " << pos.rr << endl;
		for(int i=0;i<4;i++) {
			int nx = pos.x + dir[i][0];
			int ny = pos.y + dir[i][1];
			
			if(nx < 0 || nx > n-1 || ny < 0 || ny > m-1 || s[nx][ny] == '*' || vis[nx][ny]) continue;
			vis[nx][ny] = 1;
			if(i == 0 || i == 1) {
				q.push_front(Node(nx,ny,pos.rl,pos.rr));
				vis[nx][ny] = 1;
				ans++;
			}
			else if(i == 2) {
				if(pos.rl < 1) continue;
				q.push_back(Node(nx,ny,pos.rl-1,pos.rr));
				vis[nx][ny] = 1;
				ans++;
			}
			else {
				if(pos.rr < 1) continue;
				q.push_back(Node(nx,ny,pos.rl,pos.rr-1));
				vis[nx][ny] = 1;
				ans++;
			}
		}
	}
	cout << ans << endl;
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	solve();
	return 0;
}  

例题3:USACO08JAN Telephone Lines S

题目描述
1 1 1 N N N选一条路径,对于所有的边,可以选择 k k k 条使其权值为0,求剩余边的最大权值的最小值。

分析
  • 你是否疑惑,本题与01bfs如何产生联系?所以我们要进行的就是题意的转化
  • 最大值的最小,首先考虑二分答案。对于当前的最大边权 x x x ≤ x \leq x x的边权我们随意的走(并不影响答案), > x > x >x 的权值我们尽可能让其免费,只需统计可以被优化掉的边权的数量,保证 e d g e s ≤ k edges \leq k edgesk 即可。
  • 那如何统计边的数量呢?不妨记权值 > x >x >x 的边权为 1 1 1,权值 ≤ x \leq x x的边权为 0 0 0,此时使用双端队列bfs求解转化后权值的最短路,即为当前情况下至少需要免费优化的边数(最优),则最短路中的 dis[] 数组记录的就是优化的边权数量 e d g e s edges edges
代码
#include<bits/stdc++.h>
using namespace std;

#define mem(a, b) memset(a, b, sizeof(a))
const int inf = 0x3f3f3f3f;
const int maxn = 1e3+5;

struct Edge {
	int v, w;
	Edge(){}
	Edge(int v, int w):v(v),w(w){}
};

vector<Edge> G[maxn];
void add(int u, int v, int w) {G[u].push_back(Edge(v,w));}

int dis[maxn], vis[maxn];
int n,p,k;

int check(int x) {
	mem(vis, 0); mem(dis, 0x3f);
	deque<int> dq;
	dq.push_front(1);
	dis[1] = 0;
	
	while(!dq.empty()) {
		int t = dq.front(); 
		dq.pop_front();
		if(vis[t]) continue;
		vis[t] = 1;
		
		for(auto ed : G[t]) {
			int v = ed.v;
			int w = ed.w > x; // 权值转化 0 or 1
			if(dis[v] > dis[t] + w) {
				dis[v] = dis[t] + w;
				if(w) dq.push_back(v);
				else dq.push_front(v); 
			}
		}
	}
	if(dis[n] == inf) return -1; // 1~n无通路
	if(dis[n] <= k) return 1;
	return 0;
}

void solve() {
	cin >> n >> p >> k;
	int l=1,r=1;
	for(int i=0;i<p;i++) {
		int u,v,w; cin >> u >> v >> w;
		r = max(r, w);
		add(u, v, w);
		add(v, u, w);
	}
	bool flag = true;
	while(l < r) {
		int mid = l + (r-l>>1);
		int res = check(mid);
		if(res == -1) {
			flag = false;
			break;
		}
		if(res == 1) r = mid;
		else l = mid+1;
		//cout << l << " " << r << endl;
	}
	cout << (flag ? l : -1) << "\n";
}

int main() {
	ios::sync_with_stdio(false); cin.tie(0);
	solve();
	return 0;
}  

练习


有向无环图

定义

  • 图由顶点和连接这些顶点的边所构成。每条边都带有从一个顶点指向另一个顶点的方向的图为有向图。有向图中的道路为一系列的边,系列中每条边的终点都是下一条边的起点。如果一条路径的起点是这条路径的终点,那么这条路径就是一个环。有向无环图即为没有环出现的有向图,简称DAG(directed acyclic graph)

拓扑排序

定义

  • 拓扑排序的英文名是 Topological sorting。
  • 拓扑排序要解决的问题是给一个有向无环图的所有节点以线性方式排序,使得对于任何的顶点 u u u v v v 的有向边 ( u , v ) (u,v) (u,v), 都可以有 u u u v v v 的前面。如果有向图中存在环路,那么就没办法进行拓扑排序。
  • 给定一个 DAG,如果从 i i i j j j 有边,则认为 j j j 依赖于 i i i。如果 i i i j j j 有路径( i i i 可达 j j j),则称 j j j 间接依赖于 i i i。拓扑排序的目标是将所有节点排序,使得排在前面的节点不能依赖于排在后面的节点。

Kahn算法

过程

  • 初始状态下,队列 Q Q Q 装着所有入度 0 0 0 的点, L L L 是一个空数组。
  • 每次从 Q Q Q 中取出一个点 u u u(可以随便取)放入 L L L, 然后将 u u u 的所有 ( u , v 1 ) , ( u , v 2 ) , ( u , v 3 ) ⋯ (u, v_1), (u, v_2), (u, v_3) \cdots (u,v1),(u,v2),(u,v3) 删除。对于边 ( u , v ) (u, v) (u,v),若将该边删除后点 v v v入度变为 0 0 0,则将 v v v 放入 Q Q Q 中。
  • 不断重复以上过程,直到队列 Q Q Q 为空。检查图中是否存在任何边,或者是否存在没有经过拓扑排序的节点,如果有,那么这个图一定有环路。数组 L L L 中顶点的顺序就是构造拓扑序列的结果。
  • 代码的核心是维持一个入度为 0 0 0 的顶点的集合。
    Topo_Example
  • 对上图拓扑排序的结果就是:2 -> 8 -> 0 -> 3 -> 7 -> 1 -> 5 -> 6 -> 9 -> 4 -> 11 -> 10 -> 12

时间复杂度

  • 假设这个图 G = ( V , E ) G = (V, E) G=(V,E) 在初始化入度为 0 0 0 的集合 Q Q Q 的时候就需要遍历整个图,并检查每一条边,因而有 O ( E + V ) O(E+V) O(E+V) 的复杂度。然后对该集合进行操作,显然也是需要 O ( E + V ) O(E+V) O(E+V) 的时间复杂度。因而总的时间复杂度就有 O ( E + V ) O(E+V) O(E+V)

实现

int n, m;
vector<int> G[maxn];
int in[maxn];  // 存储每个结点的入度

bool toposort() {
	vector<int> L;
	queue<int> Q;
	for(int i = 1; i <= n; i++) if(in[i] == 0) Q.push(i);
  	while(!Q.empty()) {
    	int u = Q.front(); Q.pop();
    	L.push_back(u);
    	for(auto v : G[u]) {
      		if(--in[v] == 0) Q.push(v);      	
    	}
 	}
  	if(L.size() == n) {
    	for(auto i : L) cout << i << ' ';
    	return true;
    return false;
}

习题:CF1385E Directing Edges

题目描述:
给定一个由有向边无向边组成的图,现在需要你把所有的无向边变成有向边,使得形成的图中没有环。如果可以做到请输出该图,否则输出"NO"。

分析
  • 考虑无解的情况:原图有向边已经出现环路 → 直接对原图通过拓扑排序判环(有向图判环
  • 考虑有解的情况:原图中给出的有向边必然形成DAG,在拓扑排序中,所有的有向边都是拓扑序小的点指向拓扑序大的点。对于有向边直接输出,对于无向边令拓扑序小的指向拓扑序大的建立有向边
代码
#include<bits/stdc++.h>
using namespace std;

#define mem(a,b) memset(a,b,sizeof(a))
const int maxn = 2e5+5;

int in[maxn], pos[maxn];
queue<int> Q;
vector<int> G[maxn];
int n, m;

void init() { // 多组样例 初始化
	mem(in,0); mem(pos,0);
	while(!Q.empty()) Q.pop();
	for(int i=1;i<=n;i++) G[i].clear();
}
// 有向图判环 模板
bool topo() { 
	int cnt = 0;
	while(!Q.empty()) {
		int t = Q.front(); Q.pop();
		pos[t] = ++cnt;
		for(auto v : G[t]) {
			if(--in[v] == 0) Q.push(v);
		}
	}
	if(cnt == n) return true;
	return false;
}

void solve() {
	init();
	cin >> n >> m;
	vector< pii > undir;
	for(int i=0;i<m;i++) {
		int op, u, v;
		cin >> op >> u >> v;
		if(op) {
			G[u].push_back(v);
			in[v]++;
		}
		else undir.push_back({u,v});
	}
	for(int i=1;i<=n;i++) if(!in[i]) Q.push(i);
	
	if(!topo()) {
		cout << "NO\n";
		return ;
	}
	
	cout << "YES\n";
	for(int u=1;u<=n;u++) { // 有向 
		for(auto v : G[u]) {
			cout << u << " " << v << "\n";
		}
	}
	for(auto p : undir) { // 无向 
		int x = p.first, y = p.second;
		int u = pos[x] < pos[y] ? x : y; // 拓扑序小
		int v = u == x ? y : x;          // 拓扑序大
		cout << u << " " << v << "\n"; 
	}
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int T; cin >> T;
	while(T--) solve();
    return 0;
}

最短路专题

最短路基本问题:

  • 在一个带权图中,点与点之间的路径的权值被定义为所经过的边的权值和。则点与点之间的最短路就是最小的路径权值。

  • 在求解某两个特定点A和B之间的最短路时,我们往往是求其中一个点A到其他所有点的最短路。以此获得A到B的最短路径长度。这种问题称之为**“单源最短路”**。常用的算法有:Dijkstra、Bellman Ford、SPFA等。

  • 另外一种,用于求解一幅图中所有点对之间的最短路,称之为多源最短路问题,通常使用Floyd-warshall算法,也称为Floyd算法。

三种算法的比较:

Dijkstra:

  • 本质是贪心
  • 单源最短路
  • 不可处理负权边(非负权图)
  • 复杂度: O ( n 2 ) O(n^2) O(n2) O ( ( n + m ) l o g n ) O((n+m)logn) O((n+m)logn)两种实现方式

Floyd:

  • 本质是动态规划
  • 多源最短路
  • 可处理负权边(任意图)
  • 复杂度: O ( n 3 ) O(n^3) O(n3)

SPFA:

  • 本质是贪心
  • 单源最短路
  • 可处理负权边
  • 判负环
  • SPFA是Bellman-Ford的一种优化,复杂度不稳定
  • SPFA在稀疏图上复杂度表现较好,实验复杂度 O ( 2 m ) O(2m) O(2m)
传送门

Dijkstra

问题描述:

  • 在图 G ( V , E ) G(V,E) G(V,E) 中,每条边的边权表示为 w w w,边所连接的两个点分别为 u u u v v v,起点为 s s s,求 s 到其他点的最短路。

算法思路:

1.初始化将s到每个点的距离设为正无穷,s到自己的距离设为0

2.利用s,以及s连出的边,去更新S到这些相连结点的距离。更新完成后,将S标记掉,以后不再利用。

3.寻找此时距离s最近,且未被标记掉的点,设为k。使用k点去更新k所连接的点。找最近其实就是找距离自己最小的(所以_不能处理负权边_),贪心思想,这里也将是后面优化的地方;

4.对于k连出去的边,进行三角优化(核心!!!),更新完毕后将k标记掉,不再利用。

5.循环n-1次,每次取最接近S的且未被标记的点去更新其他点。一旦被用于更新,则可证明此刻该点最短路已经确定。

复杂度优化:
  • 因为每次都要去寻找距离自己最小的点来更新,如果每次都暴力枚举最小的一个点,复杂度为 O ( n 2 ) O(n^2) O(n2),所以我们可以使用优先队列来维护这个最小值,把复杂度降为 O ( n l o g n ) O(nlogn) O(nlogn),也可以理解为BFS的一种推广。
    • 每次从优先队列里取出一个点,即距离自己最小的点,用这个点 u 作为中转点,该点连出去的所有边所能到达的点即为目标的优化点 v,如果满足 d i s [ v ] > d i s [ u ] + w dis[v] > dis[u] + w dis[v]>dis[u]+w说明有更小的 d i s [ v ] dis[v] dis[v],更新一下即可。
      三角优化

模板代码:

#include<bits/stdc++.h>
using namespace std;

typedef long long ll;

const int maxn = 100005;
const int maxm = 200005;

//链式前向星存图
struct Edge {
	int v,nxt;
	ll w;
	Edge(){}
	Edge(int v,ll w,int nxt):v(v),w(w),nxt(nxt){}
	bool operator < (const Edge &A) const {
		return w > A.w;
	}
}G[maxm];

int cnt,head[maxn];

void init() {
	cnt = 0;
	memset(head,-1,sizeof(head));
}

void add(int u,int v,ll w) {
	G[cnt] = Edge(v,w,head[u]);
	head[u] = cnt++;
}

int n,m;
int vis[maxn];
ll dis[maxn];

priority_queue<Edge> Q;

ll Dijkstra(int s) {
	memset(dis,0x3f,sizeof(dis));
	dis[s] = 0;
	Q.push(Edge(s,dis[s],-1));
	
	while(!Q.empty()) {
		Edge t = Q.top();Q.pop();
		int u = t.v; // 中转点
		if(vis[u] == 1) continue;
		vis[u] = 1;
		
		for(int i=head[u];i!=-1;i = G[i].nxt) { // 遍历所有要更新的点
			int v = G[i].v;
			int w = G[i].w;
			if(dis[v] > dis[u] + w) {           // 判断能否更新
				dis[v] = dis[u] + w;
				Q.push(Edge(v,dis[v],-1));
			}
		}
	}
	if(dis[n] == 0x3f3f3f3f3f3f3f3f) return -1; //判断是否存在最短路
	return dis[n];
}

int main() {
	cin >> n >> m;
	init();
	for(int i=0,u,v,w;i<m;i++) {
		cin >> u >> v >> w;
		//注意题目是有向还是无向
		add(u,v,w);
		add(v,u,w);
	}
	cout << Dijkstra(1) << endl;
}

Floyd

算法介绍:

  • Floyd算法用于求解任意两点间的最短路径,即多源最短路,当然也可以求解一些方案数,最大删除边数,以及求无向图最小环等等。

  • Floyd的思路非常简单,三层循环搞定。第一层枚举一个中转点 k k k,第二层和第三层枚举 i i i j j j,通过上述的_Dijkstra_的三角优化(如下图),不断更新最短路,由此可知,Floyd的算法复杂度较高,为 O ( n 3 ) O(n^3) O(n3),所以一般题目的 n n n几百,可以通过这一点判断是否能使用 Floyd 算法。

例1:Six Degrees of Cowvin Bacon

题目大意:

  • 如果两头牛在同一部电影中出现过,那么这两头牛的度就为1,如果这a,b两头牛没有在同一部电影中出现过,但 a,b 分别与 c 在同一部电影中出现过,那么a,b的度为2。以此类推,a 与 b 之间有 n头媒介牛,那么a,b的度为 n+1。自己到自己的度数为0.给出 m 部电影,每一部给出牛的个数,和牛的编号。问哪一头到其他每头牛的度数平均值最小,输出最小平均值乘100。

题目分析:

  • 求平均值最小,就是找某头牛到其他牛的距离最小,所以我们用floyd把两两之间的最短路求出来,然后遍历每一头牛,取一个最小的和sum,最后求解出平均数即可(具体细节详见代码)。

代码:

// 本题不支持万能头,CE爆你一脸/滑稽
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>

using namespace std;

int dis[305][305];
int t[305];// 存储每一部电影给出的信息
int main() {
    int n,m;
    cin >> n >> m;
    // init 初始化要注意把dis数组正无穷,然后自己到自己的距离为0
    memset(dis,0x3f,sizeof(dis));
    for(int i=1;i<=n;i++) dis[i][i] = 0;
    
    while(m--) {
        int x;
        cin >> x;
        for(int i=1;i<=x;i++) cin >> t[i];
        for(int i=1;i<x;i++) {
            for(int j=i+1;j<=x;j++) {
            	// 每两个之间建一条双向边 
            	dis[t[i]][t[j]] = 1;
            	dis[t[j]][t[i]] = 1;
			}
        }
    }
    
    // Floyd
    for(int k=1;k<=n;k++) {
        for(int i=1;i<=n;i++) {
            if(i == k) continue;
            for(int j=1;j<=n;j++) {
                if(i == j || j == k) continue;
                // 三角优化
                if(dis[i][j] > dis[i][k] + dis[k][j]) {
                    dis[i][j] = dis[i][k] + dis[k][j];
                }
            }
        }
    }
    
    int ans = 0x3f3f3f3f;
    // 循环找最小的和
    for(int i=1;i<=n;i++) {
        int sum = 0;
        for(int j=1;j<=n;j++) {
            if(i == j) continue;
            sum += dis[i][j];
        }
        ans = min(ans,sum);
    }
    // 这里注意,一定要先乘100,再去取平均,int类型不能整除,会舍去小数点后面的数,导致再扩大100倍后误差较大
    cout << ans*100/(n-1) << endl;
    return 0;
}

例2:Ping & Pang

题目分析:

  • 。。。

代码:

#include<bits/stdc++.h>
using namespace std;

int dis[205][205];
ll p[205][205];//边的条数 
int ans[205];

int main() {
    int n,m;
    cin >> n >> m;
    // init
    memset(dis,0x3f,sizeof(dis));
    for(int i=1;i<=n;i++) dis[i][i] = 0;
    
    // 建图
    for(int i=0;i<m;i++) {
        int u,v,w;
        cin >> u >> v >> w;
        if(dis[u][v] > w) {
            dis[u][v] = dis[v][u] = w;
        }
        p[u][v] = p[v][u] = 1;
    }
    
    // Floyd
    for(int k=1;k<=n;k++) {
        for(int i=1;i<=n;i++) {
            if(i == k) continue;
            for(int j=1;j<=n;j++) {
                if(j == k || i == j) continue;
                if(dis[i][j] > dis[i][k] + dis[k][j]) {
                    dis[i][j] = dis[i][k] + dis[k][j];
                    p[i][j] = p[i][k] * p[k][j];
                }
                else if(dis[i][j] == dis[i][k] + dis[k][j]) {
                    p[i][j] += p[i][k] * p[k][j];
                }
            }
        }
    }
    int index = 0;
    for(int a=1;a<=n;a++) {
        bool flag = false;
        for(int b=1;b<=n;b++) {
            if(b == a) continue;
            for(int c=1;c<=n;c++) {
                if(c == a || b == c) continue;
                if(dis[b][c] == dis[b][a] + dis[a][c] && p[b][c] == p[b][a] * p[a][c]) {
                    ans[index++] = a;
                    flag = true;
                    break;
                } 
            }
            if(flag) break;
        }
    }
    sort(ans,ans+index);

    if(index == 0) cout << "No important ." << endl;
    else {
        for(int i = 0; i < index; i++) {
            cout << ans[i];
            if(i == index - 1) cout << "\n";
            else cout << " ";
        }
    }
    return 0;
}

return传送门


Bellman–Ford → SPFA

Bellman–Ford 算法介绍:

  • Bellman–Ford 算法是一种基于松弛(relax)操作的最短路算法,可以求出有负权的图的最短路,并可以对最短路不存在的情况进行判断。其中「SPFA」,就是 Bellman–Ford 算法的一种实现。

  • 算法过程如下:

  • 三角优化)对于边 ( u , v ) (u,v) (u,v),松弛操作对应下面的式子: d i s ( v ) = min ⁡ ( d i s ( v ) , d i s ( u ) + w ( u , v ) ) dis(v) = \min(dis(v), dis(u) + w(u, v)) dis(v)=min(dis(v),dis(u)+w(u,v))

  • Bellman–Ford 算法所做的,就是不断尝试对图上每一条边进行松弛。每进行一轮循环,就对图上所有的边都尝试进行一次松弛操作,当一次循环中没有成功的松弛操作时,算法停止。

  • 每次循环是 O(m) 的,在最短路存在的情况下,由于一次松弛操作会使最短路的边数至少 + 1 +1 +1,而最短路的边数最多为 n − 1 n-1 n1,因此整个算法最多执行 n − 1 n-1 n1 轮松弛操作。故总时间复杂度为 O ( n m ) O(nm) O(nm)

  • 但还有一种情况,如果从 s s s 点出发,抵达一个负环时,松弛操作会无休止地进行下去。注意到前面的论证中已经说明了,对于最短路存在的图,松弛操作最多只会执行 n − 1 n-1 n1 轮,因此如果第 n n n 轮循环时仍然存在能松弛的边,说明从 s s s 点出发,能够抵达一个负环。

  • 特别注意:以 s s s 点为源点跑 Bellman–Ford 算法时,如果没有给出存在负环的结果,只能说明从 s s s 点出发不能抵达一个负环,而不能说明图上不存在负环。因此如果需要判断整个图上是否存在负环,最严谨的做法是建立一个超级源点,向图上每个节点连一条权值为 0 的边,然后以超级源点为起点执行 Bellman–Ford 算法。

Bellman–Ford 判断负环的实现

struct Edge {
	int v, w;
	Edge(){}
	Edge(int v, int w):v(v),w(w){}
};

const int inf = 0x3f3f3f3f;

vector<Edge> G[maxn];
int dis[maxn];
int n;

bool Bellman_Ford(int s) {
	memset(dis,0x3f,sizeof(dis));
	dis[s] = 0;
	bool flag; // 判断本轮是否松弛
	
	for(int i=1;i<=n;i++) {
		flag = false;
		for(int u=1;u<=n;u++) {
			if(dis[u] == inf) continue;
			// 如果到中转点的距离是无穷大,因为无穷大+常数=无穷大
			// 故不可能通过 u 来三角优化 
			for(auto ed : G[u]) {
				int v = ed.v;
				int w = ed.w;
				if(dis[v] > dis[u] + w) {
					dis[v] = dis[u] + w;
					flag = true;
				}
			}
		}
		if(!flag) break; // 没有边可以松弛,退出算法 
	} 
	// 第n轮循环仍然可以松弛,则从s点可以到达一个负环 
	return flag;
}

队列优化:洛谷P3385 负环

即 Shortest Path Faster Algorithm。

很多时候我们并不需要那么多无用的松弛操作。很显然,只有上一次被松弛的结点,所连接的边,才有可能引起下一次的松弛操作。

那么我们用队列来维护「哪些结点可能会引起松弛操作」,就能只访问必要的边了。

思路一: n n n 个点,每个点最多更新 n − 1 n-1 n1 次,即入队次数最多为 n − 1 n-1 n1,如果入队次数 ≥ n \geq n n,则说明有负环,这一点可以由spfa的松弛算法原理推导来。
思路二:如果没有负环,从1号点到每个点的最短路径应当是不存在环的;而如果存在环那它只可能是负环,且最短路径长度会在算法过程中无限增大。因此可以判断1号点到i号点的最短路径长度是否 < n <n <n(即经过的点数 ≤ n \leq n n,没有任何一个点被重复经过),来更高效地判断是否存在负环。只需记录最短路经过了多少条边,当经过了至少 n n n 条边时,说明从 s s s 点可以抵达一个负环。

实现:

#include<bits/stdc++.h>
using namespace std;

#define mem(a, b) memset(a, b, sizeof(a))

typedef long long ll;
typedef unsigned long long ull;

const int maxn = 2e3+5;
const int maxm = 3e3+5;

struct Edge {
	int v, w;
	Edge(){}
	Edge(int v, int w):v(v),w(w){}
};

vector<Edge> G[maxm << 1];
queue<int> Q;
int n,m;
int dis[maxn], vis[maxn];
int cnt[maxn]; // 记录最短路经过的边数 

void init() {
	for(int i=0;i<maxm*2;i++) G[i].clear();
	mem(dis, 0x3f);
	mem(cnt, 0); mem(vis, 0);
}

void add(int u, int v, int w) {G[u].push_back(Edge(v,w));}

bool spfa() {
	dis[1] = 0; cnt[1] = 0;
	Q.push(1); vis[1] = 1; // 入队-标记 
	
	while(!Q.empty()) {
		int u = Q.front(); 
		Q.pop(); vis[u] = 0; // 出队-去除标记 
		for(auto ed : G[u]) {
			int v = ed.v;
			int w = ed.w;
			if(dis[v] > dis[u] + w) {
				dis[v] = dis[u] + w;
				cnt[v] = cnt[u] + 1; // 记录最短路经过的边数
				if(cnt[v] >= n) return true;
				// 在不经过负环的情况下,最短路至多经过 n-1 条边
                // 因此如果经过了多于 n 条边,一定说明经过了负环
				if(!vis[v]) {
					Q.push(v);
					vis[v] = 1; // 入队-标记 
				}
			}
		}
	}
	return false;
}

void solve() {
	init();
	cin >> n >> m;
	for(int i=0;i<m;i++) {
		int u, v, w;
		cin >> u >> v >> w;
		if(w >= 0) {
			add(u, v, w);
			add(v, u, w);
		}
		else add(u, v, w);
	}
	cout << (spfa() ? "YES" : "NO") << endl;
}

int main() {
	int T; cin >> T;
	while(T--) solve();
	return 0;
}

Johnson 全源最短路径算法

引言

差分约束系统

定义

差分约束系统是一种特殊的 n n n 元一次不等式组,它包含 n n n 个变量 x 1 , x 2 , … , x n x_1,x_2,\dots,x_n x1,x2,,xn 以及 m m m 个约束条件,每个约束条件是由两个其中的变量做差构成的,形如 x i − x j ≤ c k x_i-x_j\leq c_k xixjck,其中 1 ≤ i , j ≤ n , i ≠ j , 1 ≤ k ≤ m 1 \leq i, j \leq n, i \neq j, 1 \leq k \leq m 1i,jn,i=j,1km 并且 c k c_k ck 是常数(可以是非负数,也可以是负数)。我们要解决的问题是:求一组解 x 1 = a 1 , x 2 = a 2 , … , x n = a n x_1=a_1,x_2=a_2,\dots,x_n=a_n x1=a1,x2=a2,,xn=an,使得所有的约束条件得到满足,否则判断出无解。

差分约束系统中的每个约束条件 x i − x j ≤ c k x_i-x_j\leq c_k xixjck 都可以变形成 x i ≤ x j + c k x_i\leq x_j+c_k xixj+ck,这与单源最短路中的三角形不等式 d i s [ y ] ≤ d i s [ x ] + w dis[y]\leq dis[x]+w dis[y]dis[x]+w 非常相似。因此,我们可以把每个变量 x i x_i xi 看做图中的一个结点,对于每个约束条件 x i − x j ≤ c k x_i-x_j\leq c_k xixjck,从结点 j j j 向结点 i i i 连一条长度为 c k c_k ck 的有向边。

注意到,如果 { a 1 , a 2 , … , a n } \{a_1,a_2,\dots,a_n\} {a1,a2,,an} 是该差分约束系统的一组解,那么对于任意的常数 d d d { a 1 + d , a 2 + d , … , a n + d } \{a_1+d,a_2+d,\dots,a_n+d\} {a1+d,a2+d,,an+d} 显然也是该差分约束系统的一组解,因为这样做差后 d d d 刚好被消掉。

类型一:差分约束的合法解

过程

  • 差分约束一般来说可以转化为最短路或者最长路
  • 最短路:对于 x i − x j ≤ c k x_i-x_j\leq c_k xixjck 转化为从 j j j i i i 连一条长度为 c k c_k ck 的带权有向边,设超级源点 d i s [ 0 ] = 0 dis[0]=0 dis[0]=0 并向每一个点连一条权重为 0 0 0 边,跑最短路,最短路的距离 x i = d i s [ i ] x_i=dis[i] xi=dis[i] 就是变量的一组合法解。一般使用 Bellman–Ford 或队列优化的 Bellman–Ford(俗称 SPFA,在某些随机图跑得很快)判断图中是否存在负环,最坏时间复杂度为 O ( n m ) O(nm) O(nm)
  • 最长路:对于 x i − x j ≥ c k x_i-x_j\geq c_k xixjck 转化为从 j j j i i i 连一条长度为 c k c_k ck的带权有向边,跑最长路,最长路的距离就是变量的一组合法解。
  • 不难注意到 x i − x j ≤ c k    ⟺    x j − x i ≥ − c k x_i-x_j\leq c_k \iff x_j-x_i\geq -c_k xixjckxjxick 因此大于等于和小于等于是等价的。因此,在求出合法解的问题上,最短路和最长路都可以使用,两种方法并没有本质区别。

常用的关系变形技巧(建图)

例题1:洛谷P5960 差分约束模版

代码:
struct Edge {
	int v, w;
	Edge(){}
	Edge(int v, int w):v(v),w(w){}
};

vector<Edge> G[maxn];
queue<int> Q;

int dis[maxn];
int vis[maxn], cnt[maxn];

void add(int u, int v, int w) {G[u].push_back(Edge(v,w));}
bool spfa() {
	mem(dis, 0x3f);
	
	Q.push(0); vis[0] = 1;
	dis[0] = 0;
	cnt[0] = 0;
	
	while(!Q.empty()) {
		int t = Q.front(); 
		Q.pop(); vis[t] = 0;
		
		for(auto ed : G[t]) {
			int v = ed.v;
			int w = ed.w;
			if(dis[v] > dis[t] + w) {
				dis[v] = dis[t] + w;
				cnt[v] = cnt[t] + 1;
				// 共n+1个点 最短路最多n条边 
				if(cnt[v] > n) return true;
				if(!vis[v]) {
					Q.push(v);
					vis[v] = 1;
				}
			}
			
		}
	}
	return false;
}

int main() {
	cin >> n >> m;
	for(int i=0;i<m;i++) {
		int u, v, w;
		cin >> u >> v >> w;
		add(v, u, w); 
	}
	// 超级源点
	for(int i=1;i<=n;i++) {
		add(0, i, 0); // 超级源点的边权可自定义 
	}
	if(spfa()) cout << "NO" << endl;
	else {
		for(int i=1;i<=n;i++) {
			cout << dis[i] << " \n"[i==n];
		}
	}
	return 0;
}  

例题2:洛谷P1993 小K的农场

题目描述:
求解差分约束系统,有 m m m 条约束条件,每条都以 x a − x b ≥ c k x_a-x_b\geq c_k xaxbck x a − x b ≤ c k x_a-x_b\leq c_k xaxbck x a = x b x_a=x_b xa=xb 的形式,判断该差分约束系统是否有解。

代码
#include<bits/stdc++.h>
using namespace std;

#define mem(a, b) memset(a, b, sizeof(a))
typedef long long ll;

const int inf = 0x3f3f3f3f;
const int maxn = 1e4+5;

struct Edge {
	int v, w;
	Edge(){}
	Edge(int v, int w):v(v),w(w){}
};

vector<Edge> G[maxn];
queue<int> Q;

int n,m;
int dis[maxn];
int vis[maxn], cnt[maxn];

void add(int u, int v, int w) {G[u].push_back(Edge(v,w));}

bool spfa() {
	mem(dis, 0x3f);
	dis[0] = 0; cnt[0] = 0;
	Q.push(0); vis[0] = 1;
	
	while(!Q.empty()) {
		int t = Q.front();
		Q.pop(); vis[t] = 0;
		for(auto ed : G[t]) {
			int v = ed.v;
			int w = ed.w;
			if(dis[v] > dis[t] + w) {
				dis[v] = dis[t] + w;
				cnt[v] = cnt[t] + 1;
				if(cnt[v] > n) return false; // 无解(最多n条边) 
				if(!vis[v]) {
					Q.push(v);
					vis[v] = 1;
				}
			}
		}
	}
	return true;
}

int main() {
	cin >> n >> m;
	for(int i=0;i<m;i++) {
		int op, a, b, c;
		cin >> op >> a >> b;
		if(op == 1) {
			cin >> c;
			add(a, b, -c);
		}
		else if(op == 2) {
			cin >> c;
			add(b, a, c);
		}
		else {
			add(b, a, 0);
			add(a, b, 0);
		}
	}
	// 超级源点
	for(int i=1;i<=n;i++) add(0, i, 0); 
	cout << (spfa() ? "Yes" : "No") << endl;
	return 0;
}  

类型二:差分约束的最优化问题

  • 部分题目在要求我们求出合法解的要求上,还要求我们求出一个满足某个性质的最优解
  • 结论:对于最短路所求得的解,满足 ∀ i , x i − x 0 \forall i, x_i-x_0 i,xix0 最大,对于最长路所求得的解,满足 ∀ i , x i − x 0 \forall i, x_i-x_0 i,xix0 最小。即最短路可以求出“最大解”,最长路可以求出“最小解”
  • 根据这个还可以推出,在满足 x x x 存在一个范围的情况下,如果要使 ∑ x \sum x x 最小,那么所有的 x x x 一定等价于最长路所求得的解;使 ∑ x \sum x x最大,那么就等价于最短路所求得的解。注意:并不是「就是」最短/长路求得的解,而是「等价于」最短/长路求得的解,需要根据题目的范围(比如非负),还需手动调整最后得出的解,从而满足范围。

例题1:洛谷P1250 种树

题目描述:
路边的地区被分割成块,编号 1 , 2 , … , n 1,2,\dots ,n 1,2,,n。每个部分为一个单位尺寸大小并最多可种一棵树。 m m m个居民,每个居民想在 [ u i , v i ] [u_i,v_i] [ui,vi] 之间最少种 t i t_i ti 棵树。保证居民在指定区不会种多于区域地块数的树,即 t ≤ v − u + 1 t\leq v-u+1 tvu+1。种树的各自区域可以交叉。求出能满足所有要求的最少的树的数量。
数据范围
1 ≤ n ≤ 3 × 1 0 4 1 \leq n \leq 3 \times 10^4 1n3×104 1 ≤ m ≤ 5 × 1 0 3 1 \leq m \leq 5 \times 10^3 1m5×103
1 ≤ u i ≤ v i ≤ n 1 \leq u_i \leq v_i \leq n 1uivin 1 ≤ t i ≤ v i − u i + 1 1 \leq t_i \leq v_i-u_i+1 1tiviui+1

分析
  • 以每一块上树的数量为变量较难入手,但我们可以转而使用前缀和,那么题目给出的条件就可以被转化为 p r e [ v ] − p r e [ u − 1 ] ≥ t pre[v]-pre[u-1] \geq t pre[v]pre[u1]t,同时题目要求每一块上只能种一棵树,所以我们有 0 ≤ p r e [ i ] − p r e [ i − 1 ] ≤ 1 0 \leq pre[i]-pre[i-1] \leq 1 0pre[i]pre[i1]1,以上约束条件建图,跑最长路。在这种转化的约束条件下,建图会出现负权边,只能使用SPFA,时间复杂度为 O ( N ( N + M ) ) O(N(N+M)) O(N(N+M))
  • 负权边导致不能使用 Dijkstra 算法,如果 n , m n, m n,m 再大一些呢?(爆爆爆 )那么能否转化使得图中没有负权边,从而优化时间复杂度?事实上,这是可以做到的!(能用dijkstra谁用spfa啊 )具体分析详见**「例题2」**。本题P1250的非负权边Dijkstra堆优化code
代码
// #define _CRT_SECURE_NO_WARNINGS
// 有负权边的spfa
#include<bits/stdc++.h>
using namespace std;

#define mem(a, b) memset(a, b, sizeof(a))
typedef long long ll;

const int inf = 0x3f3f3f3f;
const int maxn = 3e4+5;

struct Edge {
	int v, w;
	Edge(){}
	Edge(int v, int w):v(v),w(w){}
};

vector<Edge> G[maxn];
queue<int> Q;

int n,m;
int dis[maxn];
int vis[maxn], cnt[maxn];

void add(int u, int v, int w) {G[u].push_back(Edge(v,w));}

void spfa() {
	mem(dis, -0x3f);
	dis[0] = 0; cnt[0] = 0;
	Q.push(0); vis[0] = 1;
	
	while(!Q.empty()) {
		int t = Q.front();
		Q.pop(); vis[t] = 0;
		for(auto ed : G[t]) {
			int v = ed.v;
			int w = ed.w;
			if(dis[v] < dis[t] + w) {
				dis[v] = dis[t] + w;
				cnt[v] = cnt[t] + 1;
				// if(cnt[v] > n) return; // 无解(最多n条边) 
				if(!vis[v]) {
					Q.push(v);
					vis[v] = 1;
				}
			}
		}
	}
	return;
}

int main() {
	cin >> n >> m;
	for(int i=0;i<m;i++) {
		int u, v, t;
		cin >> u >> v >> t;
		add(u-1, v, t);
	}
	for(int i=1;i<=n;i++) {
		add(i, i-1, -1);
		add(i-1, i, 0);
	}
	spfa(); // 题目保证一定有解
	cout << dis[n] << endl;
	return 0;
}  

例题2:AT_abc216_G 01Sequence

题目描述:
一个长度为 N N N 01 01 01序列, A = ( A 1 , A 2 , … , A N ) , A=(A_1,A_2,\dots,A_N), A=(A1,A2,,AN)给出 M M M 条约束条件, L i L_i Li R i R_i Ri X i X_i Xi,使得 [ L i , R i ] [L_i,R_i] [Li,Ri] A L i , A L i + 1 , … , A R i A_{L_i}, A_{L_i+1},\dots,A_{R_i} ALi,ALi+1,,ARi中至少有 X i X_i Xi 1 1 1。求出一个合法的序列,要求 1 1 1的数量最小。
数据范围:
1 ≤ N ≤ 2 × 1 0 5 1 \leq N \leq 2 \times 10^5 1N2×105 1 ≤ M ≤ m i n ( 2 × 1 0 5 , N ( N + 1 ) 2 ) 1 \leq M \leq min(2\times 10^5, \frac{N(N+1)}{2}) 1Mmin(2×105,2N(N+1))
1 ≤ L i ≤ R i ≤ N 1 \leq L_i \leq R_i \leq N 1LiRiN 1 ≤ X i ≤ R i − L i + 1 1 \leq X_i \leq R_i-L_i+1 1XiRiLi+1

分析
  • 这题其实就是例题1:种树的抽象化模型,几乎一模一样:设 f i f_i fi表示区间 [ 1 , i ] [1,i] [1,i] 1 1 1 的数量,特别的,定义 f 0 = 0 f_0=0 f0=0,则约束条件可以等价转化为下列不等式:
  • 0 ≤ f i − f i − 1 ≤ 1 0 \leq f_i-f_{i-1} \leq 1 0fifi11 f R i − f L i − 1 ≥ X i f_{R_i}-f_{L_i-1} \geq X_i fRifLi1Xi
  • 在这样的条件下,为了使 1 1 1 的数量最少,目标就是在一组合法的 f f f 的基础上,使得 f N − f 0 f_N-f_0 fNf0 最小,如上述分析,以 ≥ \geq 建图,跑最长路(有负权边)。证明详见这位佬
  • 而区别就在于 ①数据范围扩大 ②输出序列( a [ i ] = p r e [ i ] − p r e [ i − 1 ] a[i] = pre[i]-pre[i-1] a[i]=pre[i]pre[i1]),如果还用SPFA,最坏复杂度为 O ( N ( N + M ) ) O(N(N+M)) O(N(N+M)),会很惨的TLE云剪切板:TLE。那我么该如何转化为非负权边,从而跑「Dijkstra」呢?
  • 区别于 f i f_i fi 统计区间 1 1 1 的个数,我们设 g i g_i gi 表示区间 [ 1 , i ] [1,i] [1,i] 0 0 0 的数量,特别的,定义 g 0 = 0 g_0=0 g0=0,则约束条件可以等价转化为下列不等式:
  • 0 ≤ g i − g i − 1 ≤ 1 0 \leq g_i-g_{i-1} \leq 1 0gigi11 g R i − g L i − 1 ≤ ( R i − L i + 1 − X i ) g_{R_i}-g_{L_i-1} \leq (R_i-L_i+1-X_i) gRigLi1(RiLi+1Xi)
  • 此时转化求 g N − g 0 g_N-g_0 gNg0 的最大值,且边权均为非负,故可以跑Dijkstra最短路,复杂度为 O ( ( N + M ) l o g ( N + M ) ) O((N+M)log(N+M)) O((N+M)log(N+M))
代码
struct Edge {
	int v, w;
	Edge(){}
	Edge(int v, int w):v(v),w(w){}
};
vector<Edge> G[maxn];
int n,m;
int dis[maxn], vis[maxn];

void add(int u, int v, int w) {G[u].push_back(Edge(v,w));}

struct Node {
	int id, w;
	Node(){}
	Node(int id, int w):id(id),w(w){}
	bool operator < (const Node &A) const {
		return w > A.w;
	}
}; 
priority_queue<Node> Q;

void Dijkstra() {
	mem(dis, 0x3f);
	dis[0] = 0; 
	Q.push(Node(0,dis[0]));
	
	while(!Q.empty()) {
		Node t = Q.top(); Q.pop();
		int cur = t.id;
		if(vis[cur]) continue;
		vis[cur] = 1;
		for(auto ed : G[cur]) {
			int v = ed.v;
			int w = ed.w;
			if(dis[v] > dis[cur] + w) {
				dis[v] = dis[cur] + w;
				Q.push(Node(v,dis[v]));
			}
		}
	}
}

int main() {
	cin >> n >> m;
	for(int i=0;i<m;i++) {
		int l, r, x;
		cin >> l >> r >> x;
		add(l-1, r, r-l+1-x);
	}
	// 非负权边 
	for(int i=1;i<=n;i++) {
		add(i, i-1, 0);
		add(i-1, i, 1);
	}
	Dijkstra();
	for(int i=1;i<=n;i++) {
		cout << (dis[i]-dis[i-1] ? "0" : "1") << " \n"[i==n];
	}
	return 0;
}  

例题:SCOI 2011 糖果

代码

例1:糖果

题目大意:

  • 给你 m 个关系,每次三个整数a , b , c,意思为小朋友b最多比小朋友a多c个糖果,即 b − a ≥ c b-a \ge c bac,现在让你求1号小朋友和n号小朋友在满足所有人的条件下,最多可以相差多少糖果。

题目分析:

  • 数学化一下,其实就是给你m个不等式,让你在整个系统中找到 1 和 n 最大的差分~~(不理解也没关系,接着往下看)~~
  • 那我们怎么把题目的条件转化成图呢??
  • 对于a , b , c我们从a ⟶ \longrightarrow b建一条长度为 c 的边,表示 a 和 b 相差 c 个
  • 举个例子,如下图所示:
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
  • 从1号-6号,选择上面的路径则得出 6比1 最多大 3 + 3 + 7 = 13 3 + 3 + 7 = 13 3+3+7=13个;选择下面的则得出 6比1 最多大 4 + 5 + 3 = 12 4 + 5 + 3 = 12 4+5+3=12个,因为在求解答案时必须要满足所有的条件,所以综上,6比1 最大只能大12个。
  • 所以,综综综上所述,我们只要根据题目数据建个有向图,跑一遍Dijkstra,求1-n的最短路即可。(是不是很神奇,orz)

代码:

 #include<bits/stdc++.h>
using namespace std;

typedef long long ll;
const int maxn = 100005;
const int maxm = 1000005;

struct Edge {
    int v,nxt;
    ll w;
    Edge(){}
    Edge(int v,ll w,int nxt):v(v),w(w),nxt(nxt){}
    bool operator < (const Edge &A) const {
        return w > A.w;
    }
}G[maxm << 1];

int cnt,head[maxn];

void init() {
    cnt = 0;
    memset(head,-1,sizeof(head));
}

void add(int u,int v,ll w) {
    G[cnt] = Edge(v,w,head[u]);
    head[u] = cnt++;
}

int m,n;
int vis[maxn];
ll dis[maxn];

priority_queue<Edge> Q;

ll Dijkstra(int s) {
    memset(dis,0x3f,sizeof(dis));
    dis[s] = 0;
    Q.push(Edge(s,dis[s],-1));

    while(!Q.empty()) {
        Edge t = Q.top();Q.pop();
        if(vis[t.v == 1]) continue;
        vis[t.v] == 1;

        int u = t.v;
        for(int i=head[u];i!=-1;i=G[i].nxt) {
            int v = G[i].v;
            int w = G[i].w;

            if(dis[v] > dis[u] + w) {
                dis[v] = dis[u] + w;
                Q.push(Edge(v,dis[v],-1));
            }
        }
    }

    if(dis[n] == 0x3f3f3f3f3f3f3f3f) return -1;
    return dis[n];
}


int main() {
    scanf("%d %d",&n,&m);
    init();
    for(int i=0,u,v,w;i<m;i++) {
        scanf("%d %d %d",&u,&v,&w);
        add(u,v,w);
    }

    printf("%lld\n",Dijkstra(1));
    return 0;
}

例2:Layout

题目大意:

  • n 头牛按顺序排成一列,可能存在多头牛站在一个位置~~(神奇)~~,给你 ML 个喜欢关系,表示 A 和 B 最多相距 D;再给你 MD 个反感关系,表示 A 和 B 最少要相距 D个。求解1号和n号之间可能的最大距离。如果没有不存在满足要求的情况,输出 -1;如果1和n可以相距无穷远。输出-2。

题目分析:

  • 同样,我们要先考虑一下怎样转化为图上问题。首先,对于第一个条件,大致和上一题的一样,可以转化为 B − A ≤ D B - A \le D BAD;对于反感关系,同理可以记为 B − A ≥ D B - A \ge D BAD,那我们怎么转化成刚刚那种形式呢?我们可以通过数学化的方法,两边同乘-1,即 A − B ≤ − D A - B \le -D ABD。所以这里我们建图的时候会出现负权边,所以我们就要用SPFA求一下最短路。

  • 对于题目中输出-1的情况,我们考虑一下如果不存在最短路,说明出现了负环,所以我们SPFA的过程中判一下负环

  • 如果走不到n,即 d i s [ n ] dis[n] dis[n]无法被更新到,仍是无穷大,则输出-2

代码:

戳我回目录


Tarjan三部曲

戳我回目录


二分图专题

基本概念:

  • 二分图,又称为二部图,顾名思义,就是由两个部分组成,是图论中的一种特殊模型,概念和定理较多,比较数学化。

  • 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集 ( A , B ) (A,B) (A,B),并且图中的每条边 ( i , j ) (i,j) (i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i 属于 A,j 属于 B),则称图G为一个二分图。(不理解也没关系,反正这东西很学术化,不想让人看懂)滑稽

  • 换句话说。二分图的定点可以分为两个集合 ( A , B ) (A,B) (A,B),所有边关联的顶点恰好一个属于 A,一个属于 B,并且每个集合的内部是不存在边关系的。

  • 区别二分图,关键是要看点集能否分成两个独立的点集,如下图所示:
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 这种图,以及边关系,就是二分图中建图的基本模型

专题练习传送门


例1:过山车

题目分析:

  • …想好再补
  • 所以,本题其实就是一个二分图最大匹配问题,我们用匈牙利算法来解决。

匈牙利算法思路:

  • 抢女朋友????抢男朋友????? 等我想好怎么描述。。
  • /滑稽
  • 先详见代码。。

代码:

#include<bits/stdc++.h>
using namespace std;

const int maxn = 505;
#define mem(a,b) memset(a,0,sizeof(a))

// 由于n比较小,这里直接用了邻接矩阵
// link[i]记录的是i之前是和谁匹配
int k,m,n;
int G[maxn][maxn],link[maxn],vis[maxn];

bool DFS(int u) {
    for(int i=1;i<=n;i++) {
        if(G[u][i] == 1 && vis[i] == 0) {// 如果u和i之间有关系,并且i这一轮没有被抢走
            vis[i] = 1;// 先霸占
            if(link[i] == 0 || DFS(link[i])) { // 如果i没有人要或者与i匹配的人除了i还能匹配其他人
                link[i] = u; // 就把i给u
                return true; // 说明可以匹配
            }
        }
    }
    return false;
}


int main() {
    while(cin >> k) {
        if(k == 0) break;
        mem(G,0);mem(link,0);mem(vis,0);// 多组数据别忘清空
        cin >> m >> n;
        for(int i=1,u,v;i<=k;i++) {
            cin >> u >> v;
            G[u][v] = 1;
        }
        int ans = 0;
        for(int i=1;i<=m;i++) {
            mem(vis,0);// 每一轮都要清空标记
            if(DFS(i)) ans ++;
        }

        cout << ans << endl;
    }
    return 0;
}

例2:Asteroids

题目大意:

  • 地球毁灭
  • unknown error

题目分析:

  • 别急,我还没想好咋写
  • 本题求最小点覆盖
  • 最小点覆盖 = 最大匹配

证明:

代码:

// 本题不支持万能头
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int maxn = 505;
#define mem(a,b) memset(a,0,sizeof(a))

int n,k;
int G[maxn][maxn],link[maxn],vis[maxn];

bool DFS(int u) {
    for(int i=1;i<=n;i++) {
        if(G[u][i] == 1 && vis[i] == 0) {
            vis[i] = 1;
            if(link[i] == 0 || DFS(link[i])) {
                link[i] = u;
                return true;
            }
        }
    }
    return false;
}


int main() {
    cin >> n >> k;
    for(int i=1,u,v;i<=k;i++) {
        cin >> u >> v;
        G[u][v] = 1;
    }
    int ans = 0;
    for(int i=1;i<=n;i++) {
        mem(vis,0);
        if(DFS(i)) ans ++;
    }
    cout << ans << endl;
    return 0;
}

戳我回目录


基础部分

树的直径

dfs

vector<int> G[maxn];
int d[maxn];
int pos;

void dfs(int u, int fa) {
	for(auto v:G[u]) {
		if(v == fa) continue;
		d[v] = d[u] + 1;
		if(d[v] > d[pos]) pos = v;
		dfs(v, u);
	}
}

void solve() {
	int n; cin >> n;
	for(int i=0;i<n-1;i++) {
		int u, v;
		cin >> u >> v;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	dfs(1, 0);
	d[pos] = 0;
	dfs(pos, 0);
	cout << d[pos] << "\n";
}

树形dp

过程
vector<int> G[maxn];
int dp[2][maxn];
int d=0;

void dfs(int u, int fa) {
	dp[0][u] = dp[1][u] = 0;
	for(auto v:G[u]) {
		if(v == fa) continue;
		dfs(v, u);
		int t = dp[0][v] + 1;
		if(t > dp[0][u]) {
			dp[1][u] = dp[0][u];
			dp[0][u] = t;
		}
		else if(t > dp[1][u]) dp[1][u] = t;
	}
	d = max(d, dp[0][u]+dp[1][u]);
}

void solve() {
	int n; cin >> n;
	for(int i=0;i<n-1;i++) {
		int u, v;
		cin >> u >> v;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	dfs(1,0);
	cout << d << "\n";
}

树的重心

最近公共祖先 LCA

树链部分

最小生成树 MST

例:Shichikuji and Power Grid

题目大意:

  • 有n个城市,坐标为 ( x i , y i ) (xi,yi) (xi,yi),还有两个系数ci,ki。在每个城市建立发电站需要费用ci,如果不建立发电站,要让城市通电,就需要与有发电站的城市连通。i与j之间连一条无向的边的费用是 ( k i + k j ) ∗ d i s (ki+kj)*dis (ki+kj)dis,dis为两个城市之间的曼哈顿距离,求让每个城市都通电的最小费用,并输出任意一个方案。

题目分析:

  • 建图,先把所有城市两两按题目规定的费用连边,然后建立一个虚拟源点O,O与所有城市连边,边权为在该城市建立发电站的费用,跑最小生成树就能得到答案。MST中选择的从源点出发的边表示在该点建立发电站,其他边表示城市与城市联通。

算法:kruskal(克鲁斯卡尔)

  • Kruskal算法是基于贪心的思想得到的。首先我们把所有的边按照权值先从小到大排序,接着按照顺序选取每条边,如果这条边的两个端点不属于同一集合,那么就将它们合并,直到所有的点都属于同一个集合为止。至于怎么合并到一个集合,那么这里我们就可以用到一个工具——-并查集。换而言之,Kruskal算法就是基于并查集的贪心算法。

代码:

#include<bits/stdc++.h>
using namespace std;

#define mem(a,b) memset(a,b,sizeof(a))

typedef long long ll;

const int maxn = 2005*2005;

struct Node {
	ll x,y;
	ll c,k;
}p[maxn];

struct Edge {
	int u,v;
	ll cost;
	Edge(){}
	Edge(int u,int v,ll cost):u(u),v(v),cost(cost){}
	
	bool operator < (const Edge &A) const {
		return cost < A.cost;
	}
	
}e[maxn];

struct Pair {
	int u,v;
	Pair(){}
	Pair(int u,int v):u(u),v(v){}
};

ll Get(int i,int j) {
	ll dis = 1LL * (abs(p[i].x - p[j].x) + abs(p[i].y - p[j].y));
	return dis * (p[i].k + p[j].k); 
}

int fa[maxn],n,cnt,station,edgesum;
vector<int> id;
vector<Pair> vec;

int Find(int x) {
	if(x == fa[x]) return x;
	return fa[x] = Find(fa[x]);
}

ll kruscal() {
	ll t = 0;
	for(int i=1;i<=cnt;i++) {
		int u = e[i].u, v = e[i].v;
		int fu = Find(u), fv = Find(v);
		if(fu != fv) {
			fa[fu] = fv;
			if(e[i].u == 0) {
				station++;
				id.push_back(e[i].v);
			}
			else {
				edgesum++;
				vec.push_back(Pair(e[i].u,e[i].v));
			}
			t += e[i].cost;
		}
		
	}
	
	return t;
}

int main() {
	int n;
	cin >> n;
	for(int i=0;i<=n;i++) fa[i] = i;//init
	for(int i=1;i<=n;i++) cin >> p[i].x >> p[i].y;
	for(int i=1;i<=n;i++) cin >> p[i].c;
	for(int i=1;i<=n;i++) cin >> p[i].k;
	
	cnt = 0;
	//虚边 
	for(int i=1;i<=n;i++) e[++cnt] = Edge(0,i,p[i].c);
	//建边 
	for(int i=1;i<=n;i++) {
		for(int j=i+1;j<=n;j++) {
			e[++cnt] = Edge(i,j,Get(i,j));
		}
	}
	
	sort(e+1,e+1+cnt);
	ll sum = kruscal();
	
	cout << sum << endl;
	cout << station << endl;
	for(int i=0;i<id.size();i++) cout << id[i] << " ";
	cout << endl;
	cout << edgesum << endl;
	for(int i=0;i<vec.size();i++) {
		cout << vec[i].u << " " << vec[i].v << endl;
	}
    return 0;
}

戳我回目录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值