2024.1.21 ~ 2024.2.2 集训总结

集训大纲

  • Week1:
    • 图论:拓扑排序、欧拉回路、二分图、最小生成树
    • 数据结构:并查集、堆、单调队列
  • week2:
    • 图论:连通性
    • 数据结构:线段树

图论

拓扑排序

DAG 上的点以关联性进行排序,得到一个有关联的枚举顺序。

有了这种特别的枚举顺序,使得在 DAG 上 DP 的转移过程更加合理且有序。

所以,DAG DP 拓扑排序。

另类排序顺序

Eg. 菜肴制作
要求在满足拓扑序的条件下,小编号的尽可能靠前。

小编号靠前 = 大编号靠后

不妨倒序考虑,先将大编号的向前放,最后倒序输出。

那么由于要让原本在后的在前,相当于反转拓扑序,所以建反图,然后满足大编号向前,即字典序越大越好,所以将队列改成大根堆即可。

数量关系建图

Eg. 数列恢复

有一个长度为 n 的整数数列 。

定义 si,j=k=ijak

你现在只知道所有 si,j 的符号,请恢复一个合法的数列,或指出不存在这样的数列。

si,j 表示的是区间和,那么转化一下,可以用前缀和表示区间和 sumjsumi1=si,j

那么已知 si,j 符号,sumjsumi1 的大小关系也就知道了。

通过大小关系建图,eu,v 表示 sumu<sumv,然后拓扑排序构造 sumv=max(sumu+1) 即可。

那么 si,j=0 呢?

既然 si,j=0 表示 sumi1=sumj 那么不妨改变一下节点的定义:u 表示所有值为 sumu 的点。当 si,j=0 的时候 sumi1sumj 等价,直接将其合并(用并查集维护起来),最后取值时直接取集合代表的值即可。

最后每个 ai=sumisumi1

丑陋代码
#include <bits/stdc++.h>
using namespace std;
const int N = 100 + 10;
int n;
int f[N];
char s[N][N];
vector<int> G[N], g[N];
int find (int x) {
	if (x == f[x]) return x;
	return f[x] = find(f[x]);
}
int in[N], a[N];
map<int, int> mp;

signed main() {
	ios::sync_with_stdio(0);
	while (cin >> n) {
		mp.clear();
		for (int i = 0; i <= n; i ++) f[i] = i, G[i].clear(), g[i].clear(), a[i] = 0, in[i] = 0;
		for (int i = 1; i <= n; i ++) {
			for (int j = i; j <= n; j ++) {
				cin >> s[i][j];
				if (s[i][j] == '+') {
					G[i - 1].push_back(j);
				} else if (s[i][j] == '-') {
					G[j].push_back(i - 1);
				} else {
					f[find(j)] = find(i - 1);
				}
			}
		}
		for (int i = 0; i <= n; i ++) {
			for (auto u : G[i]) {
				g[find(i)].push_back(find(u)), in[find(u)] ++;
			}
		}
		queue<int> q;
		int tot = 0, idx = 0;
		for (int i = 0; i <= n; i ++) {
			if (!mp[find(i)]) {
				if (!in[find(i)]) q.push(find(i));
				idx ++;
				mp[find(i)] = 1;
			}
		}
		while (!q.empty()) {
			int u = q.front();
			q.pop(), tot ++;
			for (auto v : g[u]) {
				a[v] = max(a[v], a[u] + 1);
				if(!(-- in[v])) {
					q.push(v);
				}
			}
		}
		if(tot != idx) cout << "NO\n";
		else {
			cout << "YES\n";
			for (int i = 1; i <= n; i ++) {
				cout << a[find(i)] - a[find(i - 1 )] << ' ';
			}
			cout << endl;
		}
	
	}
	return 0;
}

说实话,如果不知道算法是拓扑排序,这道题我绝对不知道要往图论方向想。

欧拉回路

这个知识点还是很陌生的,毕竟 GG 只给我们做了板题

定义

一条经过了图 G 所有的边仅一次的路径,即为欧拉路径(一笔画)。
特别的,当起点与终点相同时,称为欧拉回路。

判别

大前提:该图弱联通(有向)或联通(无向)

定义:inu,outu 分别表示有向图中一个点的入度和出度,du 表示一个无向图中的一个点的度数。

  1. 有向图
    当且仅当所有点 inu=outu 时,该有向图存在欧拉回路。
    当且仅当除起点和终点以外的点 inu=outu,且 in=out1in=out+1,该有向图存在欧拉路径。
  2. 无向图
    当且仅当该图仅有 0 个奇点,该图存在欧拉回路。
    当且仅当该图仅有 2 个奇点,该图存在欧拉路径。

寻找

从奇点(欧拉路径)或任意一点(欧拉回路)开始 DFS,每次经过一条边就将其删去(保证每一条边仅经过一次),然后以后序输出。

对于欧拉回路,显然这样输出实际上即为遍历顺序,并且每次回溯前一定已经将该点可达的所有环全部走过;但对于欧拉路径,遍历过程中可能会在走一部分环后走到终点并且无法继续,此时会出现回溯并重新遍历路径上尚未遍历的环,也就是方案不能仅仅按照遍历顺序。因此这时按照后序输出,就相当于首先保证遍历所有环然后再考虑剩余的路径,从而保证了正确性。

实现技巧:

  • 删边:可以使用链前,由于每次存边时,正边和反边的编号 1 即可得到另一边的编号,所以每次删除 ii1 号边即可。

考察方向:

建模

  1. 宝石装箱

    4n 颗宝石,价值分别为 14n
    宝石的颜色共有 n 种,每种颜色恰好有 4 颗宝石。
    现在要将宝石装进两个箱子,满足以下两个条件:

    • 两个箱子的宝石总价值相等;
    • 每个箱子每种颜色恰有 2 颗宝石。

    请你设计一种划分方案,数据保证一定存在符合条件的划分方案。

    条件 1 很好满足,每次选取 (i,4ni) 的宝石放入一个箱子中,(i+1,4ni1) 放入另一个箱子中即可。

    现在关键在于要满足条件 2。

    对于每组 (ci,c4ni) 将其视作边,该图一定存在欧拉回路:每种颜色必定度数为 4。又因为刚才满足条件 1 时的奇偶分组,所以求出该图的欧拉回路之后,奇偶间隔取边所对应的 (i,4ni) 即可。

构造

  1. 最少路径覆盖问题

    要求:简单路径,每条路径不能重复

    由于欧拉路径是保证不重复经过每一条边的,所以可以尝试用欧拉路径去覆盖这张图。

    若该图没有奇点,1 条欧拉路径即可覆盖完整个图。

    若该图奇点数 2,那么该图会长这样:

    image

    人类智慧

    由于握手定理,奇点数量为偶数个。

    若要使其被多条欧拉路径所覆盖,可以先将两两奇点连边,使得该图为欧拉图,然后找出该图的欧拉路径,删除其中的虚边,即为覆盖完该图需要的欧拉路径条数。

    所以最后答案即为 : cnt/2

    NKOJ P8436 守卫王国
    #include <vector>
    #include <iostream>
    #include <stack>
    #include <cstring>
    using namespace std;
    #define endl '\n'
    const int N = 5e5 + 10;
    int n, m;
    struct edge {
    	int to, nxt, id;
    }e[N];
    int h[N], idx, tmp;
    void add (int u, int v, int id) {
    	e[idx] = edge{v, h[u], id}, h[u] = idx, idx ++;
    	e[idx] = edge{u, h[v], - id}, h[v] = idx, idx ++;
    }
    vector<int> col[N];
    int bel[N], tot;
    int vis[N];
    void findltk (int u) {
    	col[tot].push_back(u), bel[u] = tot;
    	for (int i = h[u]; ~i ; i = e[i].nxt) {
    		if(!bel[e[i].to]) findltk(e[i].to);
    	}
    }
    int d[N];
    bool mark[N];
    stack<int> ans;
    void dfs (int u) {
    	for (int i = h[u]; ~i; i = e[i].nxt) {
    		if(mark[i]) continue;
    		h[u] = e[i].nxt;
    		mark[i] = mark[i ^ 1] = 1;
    		dfs(e[i].to);
    		ans.push(e[i].id);
    	}
    }
    signed main () {
    	ios::sync_with_stdio(0);
    	cin >> n >> m;
    	memset(h, -1, sizeof h);
    	while(m --) {
    		int u, v;
    		cin >> u >> v;
    		add(u, v, ++ tmp), d[u] ++, d[v] ++;
    	}
    	for (int i = 1; i <= n; i ++) {
    		if(! bel[i]) ++ tot, findltk(i);
    	}
    	int num = 0;
    	for (int i = 1; i <= tot; i ++) {
    		int cnt = 0;
    		if(col[i].size() == 1) continue;
    		for (auto u : col[i]) {
    			if(d[u] & 1) cnt ++;
    		}
    		num += max(cnt / 2, 1); // 一条链删除两个奇点 => ans >= sum(max(cnt / 2, 1))
    		// 证明: 构造证明
    	}
    	cout << num << endl;
    	for (int i = 1; i <= tot; i ++) {
    		if(col[i].size() == 1) continue;
    		int cnt = 0;
    		vector<int> odd;
    		for (auto u : col[i]) {
    			if(d[u] & 1) cnt ++, odd.push_back(u);
    		}
    		if(cnt == 0) {
    			dfs(col[i][0]);
    			cout << ans.size() << ' ';
    			while(!ans.empty()) cout << ans.top() << ' ', ans.pop();
    			cout << endl;
    		} else {
    			for (int j = 0; j < odd.size(); j += 2) {
    				if(cnt == 2) break;
    				add(odd[j], odd[j + 1], 0);
    				d[odd[j]] ++, d[odd[j + 1]] ++, cnt -= 2;
    			}
    			int st;
    			for (auto u : col[i]) if(d[u] & 1) st = u;
    			dfs(st);
    			while(1) {
    				vector<int> ve;
    				while(!ans.empty() && ans.top() != 0) ve.push_back(ans.top()), ans.pop();
    				cout << ve.size() << ' ';
    				for (auto v : ve) cout << v << ' ';
    				cout << endl;
    				if(ans.size()) ans.pop();
    				if(ans.empty()) break;
    			}
    		}
    	}
    	return 0;
    }
    

生成树

常用算法:

  • kruskal (加边、O(mlog2m)
  • prim (加点、O(n2)、适合完全图)

考察方向

生成树上操作

一般和数据结构绑在一起,在求出最小生成树后,根据最小生成树的性质,求解问题。

  1. 最小生成树边分类

    判断每条边是一定出现在最小生成树上还是可能还是不可能

    首先先将任意一颗最小生成树找出来,然后讨论每条非树边。

    1. 如果该边连接的两点在生成树上的边中存在边长与其相等的边,那么该边可以替换这些与其边长相等的边,此时用倍增标记所有与其相等的边,它们也不是唯一的了。
    2. 否则,这条边一定不存在最小生成树上。
  2. 次小生成树
    首先,次小生成树有一个重要的性质:它与最小生成树的差别只有一条边。

    证明:
    假设将边 e1e2 均改为 其的边 e3e4,可以得到次小生成树。
    那么,记除去 e1,e2 的边权和为 res,即 res+e1+e2res+e3+e4
    resres+e3e1+e4e2
    又因为 e3e1,e4e2 所以后边的书均大于等于 0,那么只加一个肯定小于等于加两个,所以只改一条边更优。
    那么,对于每一条非树边,该边连接的两点在生成树上的边中找出以该边替换后增量最小的,即路径最大值,然后取每条非树边替换后结果的最小值即可。同样的,路径最大值也可以使用倍增维护。

建模

  1. 跨越雷区
    一个点与两个雷区距离的最小值最大化,即这两个雷区距离 /2,所以为了让与雷区距离最大值最小化,不妨连接所有雷区,然后找出最小生成树上最大边,即为路径上最大值距离 ×2,答案 \2 即可。(注意任意雷区可互达,所以是完全图,使用 Prim 算法)

连通性

最关键的:DFS 树

强连通分量

定义不再赘述(虽然上一次总结的时候也没写)。

在处理有环有向图的问题时,如果缩点后每个强连通分量里的信息可以方便处理出来的化,不妨先缩点,然后处理强连通分量内部信息,再在新的 DAG 上,进行拓扑排序、最短路……化繁为简。

双连通分量

该知识点掌握不牢,很多题不会做

性质

边双和点双缩点后均得到一棵树

  • 点双
    • 每个 size>2 的点双中,每一条边都在一个简单环中,特别的,当点数 = 边数时,该点双中的所有边都仅在一个简单环中。
    • 点双中任意两个点都存在两条经过点不相同的路径。
    • 点双不具有传递性。
    • 非割点仅属于一个点双中。
    • 每条边仅属于一个点双中。
  • 边双
    • u,v 边双联通即两点间没有必经边。
    • 边双联通具有传递性,u,vv,w 边双联通,那么 u,w 边双联通。
    • 对于边双内任意一条边 (u,v),存在经过 (u,v) 的回路。
    • 对于边双内任意一点 u,存在经过 u 的回路。

解题

如果看到题目中出现:删去某一点,删去某一边,可以往点双或边双的方向想。

az……真写不出来了。

DS

DSU

维护许多树形集合的一种数据结构,复杂度接近常数。

支持操作:

  • 合并
  • 删除(需要建虚点)

优化:

  • 路径压缩(几乎必备)
  • 按秩合并

难点:

本体并不算很难,难在运用和建模,一般维护父子结构时可以使用。

单调队列

O(n) 时间内求出每一个长度为 k 的区间的极值。

同样,本体没什么花样,一般用于优化 DP。

这里重点说优化 DP。

对于形如 dpi=maxj=iki1dpj+ai+bj 的转移方程,首先尽可能将与变量 j 不相关的常数移动至循环外,使得循环内的只与 j 有关,那么此时,循环内的即可用单调队列维护出来。

线段树

本体可以出得很花,运用也可以很广。

本体

一些区间操作,区间查询的裸题,专门考验对线段树的熟练程度。

可能出询问的东西很奇怪,但是可以线段树区间合并来合并出答案的问题。

运用

好像我真的不会用

可以优化 DP,即区间 max,min, 操作时可以使用线段树,一般不需要变动转移方程,直接 updata(dp[i]) 原值即可。

其它的真的不会了。

posted @   固态H2O  阅读(100)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示