8.2 KiB
选择题
7
邻接表(node{vector}})
\neq
邻接矩阵(array[n][n])
8
二分图
节点由两个集合组成,且两个集合内部没有边的图。
二分图(Bipartite Graph)是一种特殊的图(graph),它的顶点集可以被划分为两个不相交的子集,使得每条边的两个端点分别属于这两个子集。简单来说,在二分图中,图的所有顶点可以分成两部分,且每条边都连接来自不同部分的顶点。
形式化定义:
- G = (V, E) 是一个图,其中 V 是顶点集,E 是边集。
- G 是二分图,当且仅当存在一个划分 (V1, V2),使得 V1 ∪ V2 = V 且 V1 ∩ V2 = ∅(即 V1 和 V2 不重叠),并且对于所有的 (u, v) ∈ E,都有 u ∈ V1 且 v ∈ V2 或者 u ∈ V2 且 v ∈ V1。
二分图的性质
- 无奇环:二分图中不包含奇数长度的环。如果一个图中不包含奇环,那么它一定是二分图。
- 着色:二分图是可以用两种颜色对所有顶点进行着色的图,且相邻顶点不会有相同的颜色。
二分图的应用
- 匹配问题:二分图常用于解决匹配问题(Matching),如最大匹配(Maximum Matching)和最大流问题中的二分图匹配。
- 分组问题:二分图用于建模需要将元素分为两个组的情况,比如在网络中将用户和服务器分开。
- 任务分配:在任务分配问题中,将任务和资源建模为二分图,找到最优分配方式。
如何判断一个图是否是二分图
常用的方法是染色法(BFS或DFS):
- 从任意一个顶点开始,将其染成一种颜色(如红色),然后将与其相邻的所有顶点染成另一种颜色(如蓝色)。
- 对每个未被访问的顶点重复上述步骤。
- 如果过程中发现一个顶点需要染成两种不同的颜色,则图不是二分图;否则,图是二分图。
要计算 n
个顶点的二分图最多可以有多少条边,首先需要了解二分图的结构特性。二分图的顶点集可以划分为两个不相交的子集 V_1
和 $ V_2 $,并且每条边都连接来自不同子集的顶点。因此,二分图的边数受限于这两个子集中顶点的数目。
分析
设 V_1
和 V_2
是二分图的两个顶点集,其中 |V_1| = n_1
和 $ |V_2| = n_2 ,且
n_1 + n_2 = n 。在二分图中,所有的边都连接
V_1 中的顶点和
V_2 $ 中的顶点,所以最大边数就是两个子集之间所有可能的连接数。
最大边数
最大边数 E_{\text{max}}
就是所有可能的边数,即:
E_{\text{max}} = n_1 \times n_2
要使 E_{\text{max}}
最大化,我们需要尽量均匀地分配顶点给 V_1
和 $V_2 $。
最大化边数的分配
为了最大化 $ n_1 \times n_2 ,我们应该让
n_1 和
n_2 尽量接近。数学上,给定
n = n_1 + n_2 ,如果
n 是偶数,则最佳分配是
n_1 = n/2 和
n_2 = n/2 ;如果
n 是奇数,则最佳分配是
n_1 = \lfloor n/2 \rfloor 和
n_2 = \lceil n/2 \rceil $。
根据这些分配方式,我们可以总结如下:
- 如果
n
是偶数:E_{\text{max}} = \left(\frac{n}{2}\right) \times \left(\frac{n}{2}\right) = \frac{n^2}{4}
- 如果
n
是奇数:
E_{\text{max}} = \left(\left\lfloor \frac{n}{2} \right\rfloor \right) \times \left(\left\lceil \frac{n}{2} \right\rceil \right) = \left(\frac{n-1}{2}\right) \times \left(\frac{n+1}{2}\right) = \frac{n^2 - 1}{4}
- 如果
结论
综上所述,给定 n
个顶点的二分图最多可以有:
E_{\text{max}} = \left\{
\begin{array}{ll}
\frac{n^2}{4}, & \text{如果 } n \text{ 是偶数} \\
\frac{n^2 - 1}{4}, & \text{如果 } n \text{ 是奇数}
\end{array}
\right.
这意味着当 n
个顶点均匀地分配在两个子集中时,二分图能够包含最多的边。
编程选择题
2
代码
#include <iostream>
#include <cstdlib>
using namespace std;
int n;
int d[10000];
int find(int L, int R, int k) {
int x = rand() % (R - L + 1) + L;
swap(d[L], d[x]);
int a = L + 1, b = R;
while (a <= b) {
while (a <= b && d[a] < d[L])
++a;
while (a <= b && d[b] >= d[L])
--b;
if (a < b) {
swap(d[a], d[b]);
++a;
--b;
}
}
swap(d[L], d[b]);
if (b - L + 1 == k)
return d[b];
if (b - L + 1 < k)
return find(b + 1, R, k - (b - L + 1));
return find(L, b - 1, k);
}
int main() {
int k;
cin >> n;
cin >> k;
for (int i = 0; i < n; ++i)
cin >> d[i];
cout << find(0, n - 1, k) << endl;
return 0;
}
这段代码实现了快速选择算法(Quickselect),用于在未排序的数组中查找第 k
小的元素。
代码解释
-
输入部分:
n
是数组d
的大小。k
是我们要找的第k
小的元素。- 用户从输入中读取
n
和k
,然后读取n
个整数填充数组d
。
-
find
函数:- 这个函数通过递归的方式实现了快速选择算法。其参数
L
和R
定义了搜索的区间,k
是要查找的第k
小元素。
- 这个函数通过递归的方式实现了快速选择算法。其参数
-
算法的核心逻辑:
- 随机选取一个枢轴(
x
),将这个枢轴的值与d[L]
交换,使得枢轴在d[L]
处。 - 使用两个指针
a
和b
,分别从L+1
向右和从R
向左遍历,重排元素使得枢轴左边的元素都小于它,右边的元素都大于它。 - 最后,交换
d[L]
和d[b]
,使得d[b]
成为枢轴元素,并且d[b]
的位置是它在排序后应有的位置。 - 根据枢轴的位置
b
与k
的关系:- 如果
b - L + 1 == k
,那么d[b]
就是我们要找的第k
小的元素。 - 如果
b - L + 1 < k
,则说明第k
小的元素在枢轴右侧,因此递归搜索b + 1
到R
区间,寻找第k - (b - L + 1)
小的元素。 - 如果
b - L + 1 > k
,则第k
小的元素在枢轴左侧,因此递归搜索L
到b - 1
区间。
- 如果
- 随机选取一个枢轴(
-
程序的执行流程:
- 主函数
main
读取输入后调用find
函数,输出结果即为数组中第k
小的元素。
- 主函数
代码功能总结
该代码的主要功能是在一个未排序的数组中寻找第 k
小的元素,使用快速选择算法,它的平均时间复杂度为 O(n)
。
如果数组中的数字是单调递增或单调递减,find
函数仍然可以正确地找到第 k
小的元素,但在最坏情况下,算法的性能可能会变差。
单调递增的情况
在数组单调递增的情况下:
- 假设数组是
[1, 2, 3, ..., n]
,且我们要找第k
小的元素。 - 在每次调用
find
函数时,随机选择的枢轴x
如果总是选择d[L]
或者接近d[L]
,将会导致以下情况:- 枢轴的值最小,每次都会将整个数组的一部分分给左边,右边是空的(
a
会从L+1
一路增加到R+1
)。 - 因此,每次递归只减少一个元素,导致
find
函数的递归深度达到n
,退化成类似于选择排序的行为,时间复杂度变为O(n^2)
。
- 枢轴的值最小,每次都会将整个数组的一部分分给左边,右边是空的(
单调递减的情况
在数组单调递减的情况下:
- 假设数组是
[n, n-1, n-2, ..., 1]
,且我们要找第k
小的元素。 - 每次调用
find
函数时,随机选择的枢轴x
如果总是选择d[L]
或者接近d[L]
,会有类似单调递增的情况:- 枢轴的值最大,所有其他元素都被分到左边,右边为空(
b
会从R
一路减少到L
)。 - 每次递归仍然只减少一个元素,导致递归深度达到
n
,时间复杂度也退化为O(n^2)
。
- 枢轴的值最大,所有其他元素都被分到左边,右边为空(
总结
虽然快速选择算法在随机数据或一般情况下具有 O(n)
的平均时间复杂度,但在输入数据是单调递增或单调递减且每次选择的枢轴都很不理想(例如总是选到最小或最大值)时,算法可能退化到最坏的情况,时间复杂度变为 O(n^2)
。
要改善这种情况,可以采用随机化策略选择枢轴(如现在代码中所做的)或者使用“三点取中”的方式选择枢轴,以减少最坏情况发生的概率,从而保证算法的效率。