LJF 清北图论 Day1 笔记
LJF 清北图论 Day1 笔记
一、从一个简单的问题开始
现在有奇数个人,两两间可能认识或者不认识,证明永远存在一个人认识偶数个人
由于两个人只有认识和不认识两种关系,也叫做布尔关系,我们可以把这个问题抽象成图的问题
那什么是图?
图由点集和边集构成,确定有哪些点和哪些边就可以确定一张图
我们称一个点连接的点数为这个点的度数
那么这个题就转换为了,给定一张奇数个点的图,求证存在一个度数为偶数的点
我们考虑拓展一下命题,可以得到如下:
给定一张奇数个点的图,求证存在奇数个度数为偶数的点
考虑变化一下,得到如下命题:
给定一张奇数个点的图,求证存在偶数个度数为奇数的点
证明出来这个命题就可以一路推到上面
考虑怎么证明
我们求度数和,每条边的贡献是 $ 2 $,所以度数和一定是偶数,然后我们把所有的点分成两部分,一部分是度数为奇数的点,另一部分是读数为偶数的点,然后因为度数为偶数的点的度数和一定是偶数,所以我们有度数是奇数的点的度数和是偶数,所以度数是奇数的点有偶数个
证完了
这道题就做完了
二、联通
定义
一个块中所有的点可以互相到达,叫做联通块
一个图由一个联通块构成,叫做连通图
一个简单的证明
证明:一个 $ n $ 个点和至少 $ (n - 1)(n - 2) / 2 + 1 $ 条边的图是连通图
考虑一个简单的性质:一个 $ n - 1 $ 个点的图,至多有 $ (n - 1)(n - 2) / 2 $ 条边,这个可以利用组合数学去思考,也就是每条边和其他的 $ n - 1 $ 条边连接,就是 $ n(n - 1) $,然后每条边计算了两次,除以 $ 2 $ 即可,这里有 $ n - 1 $ 个点,计算方法一样
这 $ n - 1 $ 个点的图一定是联通的
那我们考虑 $ n $ 个点的图,$ (n - 1)(n - 2) / 2 + 1 $ 条边的图是连通图
这并不显然,所以我们考虑证明
如果分成两半,一半是 $ k $,一半是 $ n - k $,考虑最少得边,一定是 $ \frac{k(k - 1)}{2} + \frac{(n - k)(n - k - 1)}{2} + 1 $
证明这个比刚刚的大就可以了,如果比他大就可以证明 $ (n - 1)(n - 2) / 2 + 1 $ 是最小的,然后就得证了
拆开手算即可
另一个证明
一个图和它的补图至少有一个是联通图
考虑这么证明
假设有两个点集互不联通,然后这个图的补图就可以使得这两个点集互相联通,也就是这个图的补图中,其中一个点集要向另一个点集分别连边,这不就联通了吗?然后这就是一个连通图
否则的话原图是联通图
证完了
又是一个证明
房间里有 $ (m - 1)n + 1 $ 个人,证明要么存在 $ m $ 个人两两不认识,要么存在一个人至少认识 $ n $ 个人
我们考虑如果不满足其中一个条件,一定满足另一个条件
我们假设第二个条件不成立,那么一定存在 $ m $ 个人两两不认识
我们根据假设,可以得到,每个人最多认识 $ n - 1 $ 个人,然后我们考虑把其中一个点认识的人删掉,也就是说最多会删掉 $ n - 1 $ 个点,这件事我们干 $ m - 1 $ 次,就会剩下 $ m $ 个点,这 $ m $ 个点一定没有边
可以自己画画试试
如果不满足第二个条件不成立,那么第二个条件就成立,也就是说,第二个条件只有成立和不成立,而不成立的话另一个条件又一定成立,所以这个命题是成立的
再来一个证明
连通图 $ G $ 满足任意两条边至少有一个点是重合的,证明 $ G $ 要么是 $ 3 $ 个点的完全图,要么是星星(菊花图)
我们考虑这么证
首先菊花图的话要保证只有一个点度数大于等于 $ 2 $,而菊花图是满足条件的,和上面的题一样,我们考虑不是菊花图的情况
首先如果没有点度数大于等于 $ 2 $,也就是说只有一条边,有保证是连通图,所以这种简单的情况就是两个点一条边,不再考虑了
如果有两个点的度数大于等于 $ 2 $,那么我们就可以发现,这两个顶点分别引出一条边,就不满足至少一个点重合的条件,唯一的特例是三个点的完全图,可以自己想一下
然后就证完了
这是最后的证明了
证明一个连通图山区任意一个点连通性不变
考虑如果存在度数是 $ 1 $ 的点,删掉就可以了
如果不存在,找一个起点,删掉距离这个点最远的点就可以了
可以自己想一想正确性
三、欧拉路和欧拉回路相关证明
欧拉路:在一个图中,把所有边都正好走一次的路径
欧拉回路:起点终点同为一个点的欧拉路
欧拉回路的证明
考虑找一个点,这个点到出去转一大圈再回来,这样经过这个点连接的两条边,也就是说,这样构造左右的点的度数必须是偶数
然后就证出了欧拉路的构造,也就是说,只需要做一个 DFS 就可以了
欧拉路
可以存在两个度数是奇数的点,从一个点进入,从另一个点出去,这样可以和之前一样的方式构造出欧拉路
DFS 的时候要指定起点
四、哈密顿回路相关证明
经过所有点恰好一次的路径就叫做哈密顿回路
先看一个简单的证明
有 $ n + 1 $ 个不同的 $ \leq 2n $ 的正整数,有两个数互质
证明:很显然,$ n $ 和 $ n + 1 $ 是互质的,$ n + 1 $ 个数一定有两个数相连,然后就证完了
一个小定理的证明
$ n $ 个点,若每个点度数 $ \geq \frac{n}{2} $,一定存在哈密顿回路
反证,假设不存在哈密顿回路
那我们考虑原图是 $ G $,然后在原图中加边,得到一个 $ G' $,使得任意一条不在 $ G' $ 中的边 $ e $,有 $ G' + e $ 存在哈密顿回路,也就是说,加入所有不影响哈密顿回路存在的边
然后我们考虑那些不在 $ G' $ 中的边,比如说 $ e $,假设连接了 $ v_1 - v_n $,也就是说,在 $ G' $ 中一定有 $ v_1 - v_2 - v_3 - \dots - v_{n - 1} - v_n $
这里我们可以得到,如果 $ G' $ 中,$ v_1 $ 和 $ v_i $ 有边,那么 $ v_n $ 和 $ v_{i - 1} $ 一定没有边,不然存在哈密顿回路,可以自己画画试试
这样的 $ i $ 至少存在 $ \frac{n}{2} $ 个,因为 $ 1 $ 的度数至少是 $ \frac{n}{2} $,然后在 $ v_n,v_{i - 1} $ 中,这个 $ i - 1 $ 也至少有 $ \frac{n}{2} + 1 $ 个,所以 $ v_n $ 加上自己,有 $ \frac{n}{2} $ 个点不能连接,这样它的度数就 $ < \frac{n}{2} $,与已知条件不符
所以一定存在哈密顿回路
证完了
一个类似的证明
任意两个不相邻的点,度数和 $ \geq n $,那么这个图存在哈密顿回路
证明的方法差不多,其实可以想象把上一个证明的已知条件中每个点度数 $ \geq \frac{n}{2} $,把两个点的度数加起来,就 $ \geq n $,不相邻是因为有公共边
难度较大的证明
给定一个 $ 4 \times n $ 的棋盘,里面有一个马,证明没有经过所有点的回路,也就是哈密顿回路
首先自己模拟一下就知道没有,然后考虑证明
我们把所有点标成黑白的,很显然,一个点无论怎么跳,一定从黑跳到白,白跳到黑(一共 $ 8 $ 种跳法,自己画画),然后就可以发现想跳哈密顿回路,一定要跳奇数次,但是奇数次一定会跳到和起点不一样的颜色,所以跳不回起点
五、图的存储和遍历
邻接矩阵
一种简单的方法,我们使用邻接矩阵,用一个数组 bool A[][]
来存储,如果点 $ i $ 和 $ j $ 有边,那么 $ A_{i,j} = 1 $,否则是 $ 0 $
蛋这种方法在稀疏图里并不高效,$ O(n) $ 遍历,空间复杂度是 $ O(n^2) $
邻接表
这里使用 vector 去优化,我们把一个点连接的其他点放在一个 vector 里面,开 $ n $ 个 vector
代码:
vector < int > Edge[1005];
inline void add (int u, int v) {
Edge[u].push_back (v);
Edge[v].push_back (u);
}
inline void search (int u) {
for (int i = 0; i < Edge[u].size (); ++ i) {
int v = Edge[u][i];
}
}
链式前向星
深受广大 OIer 喜爱的存储方式,这里利用链表代替 vector,全部放到了一个一维数组,只存边,空间复杂度很优,是 $ O(m) $ 的
int head[1005], next[1005], to[1005], cnt;
inline void add (int u, int v) {
to[++ cnt] = v;
next[cnt] = head[u];
head[u] = cnt;
to[++ cnt] = u;
next[cnt] = head[v];
head[v] = cnt;
}
inline void search (int u) {
for (int i = head[u]; i; i = next[i]) {
int v = to[i];
}
}
CF1472C
这个题虽然不需要图论去做,但是因为这是图论课,所以我们需要使用图论(乐
首先我们考虑建图,一个点 $ i $,和点 $ i + a_i $ 连边,相同的,点 $ i + a_i $,就要和 $ i + a_i + a_{i + a_i} $,连边
然后遍历所有的链就可以得到答案
代码:
# include <bits/stdc++.h>
using namespace std;
inline void solve () ;
signed main () {
int T = 1;
cin >> T;
while (T --) {
solve ();
}
return 0;
}
int n, a[200005];
vector < int > Edge[200005];
int vis[200005];
inline int dfs (int u) {
if (u > n || vis[u]) return 0;
vis[u] = 1;
if (Edge[u].size () == 1) return a[u] + dfs (Edge[u][0]);
else return a[u];
}
inline void solve () {
cin >> n;
for (int i = 1; i <= n; ++ i) vis[i] = 0, Edge[i].clear ();
for (int i = 1; i <= n; ++ i) cin >> a[i];
for (int i = 1; i <= n; ++ i) Edge[i].push_back (i + a[i]);
int ans = 0;
for (int i = 1; i <= n; ++ i) ans = max (ans, dfs (i));
cout << ans << endl;
return ;
}
六、二分图
二分图就是左边一列点,右边一列点,然后有一些边连接它们
如果左边选一个点,右边选一个点,保证一个点只选一次,并且左右的点有连边,就是一个正确的匹配
二分图最大匹配:最多的匹边变数量
归纳法证明简单例子
$ 1^2 + 2^2 + \dots + n^2 = \frac{n(n + 1)(2n + 1)}{6} $
首先,我们考虑当 $ n = 1 $。等式成立
然后我们考虑,如果 $ 1^2 + 2^2 + \dots + n^2 = \frac{n(n + 1)(2n + 1)}{6} $ 成立,能否推出 $ 1^2 + 2^2 + \dots + (n + 1)^2 = \frac{(n + 1)(n + 2)(2n + 3)}{6} $
多项式简单算算就可以了
然后我们考虑从 $ n = 1 $ 推到 $ n $,就可以推出来
这就是归纳法
一个定理的证明
在左边一列点集 $ A $ 中,找一个点集 $ S $,把 $ S $ 里面的点可以到达的右边一列点集 $ B $ 中的点组成的点集为 $ T $
如果对于任意的 $ S $,以及 $ T $,都满足 $ | T | \geq | S | $,一定存在一个完全匹配
如果集合 $ A,B $ 大小为 $ 1 $,定理一定满足
如果 $ A,B $ 大小是 $ n $,并且定理满足,考虑怎么推出 $ n + 1 $ 时满足
咕咕
匈牙利算法
用于求二分图最大匹配
这里相当于一个 DFS 的过程,尝试每一种选择,如果不行就回溯
我们可以考虑枚举左边一列的点,然后尝试第一种选择,往下走,选过的右边的点就标记,如果没有可以选择的,就看看有没有备选过的点,然后尝试修改之前的选择,取一个当前的的最大值
然后代码如下:
# include <bits/stdc++.h>
# define int long long
using namespace std;
int n, m, e, match[10005];
bool vis[10005];
vector < int > Edge[10005];
inline bool dfs (int u) {
if (vis[u]) return 0;
vis[u] = 1;
for (int i = 0; i < Edge[u].size (); ++ i) {
int v = Edge[u][i];
if (! match[v] || dfs (match[v])) {
match[u] = v;
match[v] = u;
return 1;
}
}
return 0;
}
signed main () {
cin >> n >> m >> e;
for (int i = 1; i <= e; ++ i) {
int u, v; cin >> u >> v;
Edge[u].push_back (v + n);
Edge[v + n].push_back (u);
}
int ans = 0;
for (int i = 1; i <= n; ++ i) {
memset (vis, 0, sizeof (vis));
if (dfs (i)) {
++ ans;
}
}
cout << ans << endl;
return 0;
}
CF1764C
这道题非常简单,我们可以考虑以下规则:
如果 $ i $ 连向 $ j $,那么 $ j $ 就不能连向更大的点,同样,$ i $ 也不能连向更小的点,所以我们可以考虑 $ 1 $ 到 $ i $ 和 $ j $ 到 $ n $ 分别连编,这样可以使得答案最优,这样我们可以利用乘法原理来求得最大的答案
要求保证 $ a_i \neq a_{i + 1} $
这里我们用 $ T_i $ 记录连续的 $ a_i $ 相等的数有几个
然后每次枚举的时候加上 $ T_i $ 就可以避免重复,计算答案也方便了许多
代码如下:
# include <bits/stdc++.h>
# define int long long
using namespace std;
int t;
int n, a[200005];
int cnt, T[200005];
signed main () {
cin >> t;
while (t --) {
cin >> n;
for (int i = 1; i <= n; ++ i) cin >> a[i];
sort (a + 1, a + 1 + n);
int ans = -1;
cnt = 0;
memset (T, 0, sizeof (T));
for (int i = 1; i <= n; ++ i) {
if (a[i] != a[i - 1]) ++ cnt;
++ T[cnt];
ans = max (ans, T[cnt] / 2);
}
int p = 0, q = n;
for (int i = 1; i <= cnt; ++ i) {
p += T[i], q -= T[i]; // q = n - p;
ans = max (ans, p * q);
}
cout << ans << endl;
}
return 0;
}
七、树
边数等于点数 $ - 1 $ 的连通图叫做树
一个性质:任意两点的路径唯一
另一个性质:一个树一定存在度数为 $ 1 $ 的点,叫做叶子节点
证明很好证,反证,如果不存在,也就是说,度数至少为 $ 2 $,那么我们可以考虑总度数是 $ 2n $,边数至少为 $ n $,矛盾了
所以存在
存储
和图一样
遍历
这里使用 DFS 遍历
代码:
vector < int > Edge[10005];
inline void dfs (int u, int fa) {
for (int i = 0; i < Edge[u].size (); ++ i) {
int v = Edge[u][i];
if (v == fa) continue;
dfs (v, u);
}
}
CF522A
把字符串当成节点建一颗树,然后遍历找深度最大的点就可以了
CF115A
咕咕
树的直径
树的直径就是树上的最长路径
这里有一个很简单的做法
从任意一个点 DFS,找一个最远的点,然后再找到这个点再做一次 DFS,找最远的点,这两个点连起来就是直径
代码如下:
树的重心
树的重心就是找一个点,删去以后使得剩下的 $ k $ 个连通分量点数最多的最少
这个比较简单,枚举点就可以了,可以做一遍 dfs 来解决
代码如下:
咕咕
CF690C2
咕咕