永恒的码流

万物皆流,无物常驻

0%

介绍

基本思想和策略

基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。

由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。

与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。

$$ f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)} $$

通俗理解

动态规划即要求最值,求解动态规划的核心问题是穷举!列出所有可能的答案,然后找出最值!

一,穷举;二,利用备忘录聪明的穷举。

  • 动态规划的穷举有点特别,因为这类问题存在「重叠子问题」,如果暴力穷举的话效率会极其低下,所以需要「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。
  • 动态规划问题一定会具备「最优子结构」,才能通过子问题的最值得到原问题的最值。
  • 动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,只有列出正确的「状态转移方程」,才能正确地穷举。

核心在于:

  • 确定基本例
  • 确定状态,状态的参数。
  • 确定选择,即达到当前状态有几种方式,从这几种方式中选出最优解。
  • 明确 dp 函数/数组的定义。

实践

轮流选择问题

状态:先手最优解。具体*dps[i][j]*表示区间*i~j*先手获得的最大值或差值等。

转移:先手最优即次手最差,$dp[i][j] = sum[i][j] - Math.min(dp[i + 1][j], dp[i][j - 1]);$

情景:

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 示例:预测赢家,状态为先手的分数
public boolean PredictTheWinner(int[] nums) {
int n = nums.length;
int[][] dps = new int[n][n];
int[][] sums = new int[n][n];
for (int i = n - 1; i >= 0; i --) {
for (int j = i; j < n; j ++) {
if (i == j) {
dps[i][j] = nums[i];
sums[i][j] = nums[i];
} else {
sums[i][j] = sums[i][j-1] + nums[j];
dps[i][j] = sums[i][j] - Math.min(dps[i + 1][j], dps[i][j - 1]);
}
}
}
return dps[0][n-1] >= sums[0][n-1] - dps[0][n-1];
}

最长子序列问题

状态一:*dp[i]*表示,以*i*结尾的最大长度

状态二:*dps[i][j]表示,单序列时\*i~j\*间最长子序列长度,双序列时长度分别为0~i*和*0~j*的序列最长公共子序列长度或其他值。

最小路径和类

股票交易问题

状态:*dps[i][j][k]表示i天内j*次交易处于k(持有或不持有股票)状态时的最大利润。

变形:j可以为1次、无限次、k次;k可以添加格外的冷冻期等状态;可增加手续费

边界值:

dp[-1][k][0] = dp[i][0][0] = 0

dp[-1][k][1] = dp[i][0][1] = -infinity

状态转移方程,买的时候算交易一次:

不持有:$dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])$

持有时:$dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])$

情景:

  • leetcode 121. 买卖股票的最佳时机 最多可以交易1次
  • 剑指 Offer 63. 股票的最大利润,最多交易1次
  • leetcode 122. 买卖股票的最佳时机 II 最多可以交易无限次
  • leetcode 123. 买卖股票的最佳时机 III 最多可以交易2次
  • leetcode 188. 买卖股票的最佳时机 IV 最多可以交易k次
  • leetcode 309. 最佳买卖股票时机含冷冻期 最多可以交易无限次
  • leetcode 714. 买卖股票的最佳时机含手续费 最多可以交易无限次

背包问题

状态:*dp[i][j]表示前i个物品满足目标j*的最优解。

背包问题

参考

简介

排序算法是《数据结构与算法》中最基本的算法之一。

排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有,分类如下:

  • 选择排序、冒泡排序
  • 归并排序、快速排序
  • 插入排序、希尔排序
  • 计数排序、桶排序、基数排序
  • 堆排序

algorithm-10-sort

阅读全文 »

简介

最短路径问题是图论研究中的一个经典算法问题,旨在寻找图(由结点和路径组成的)中两结点之间的最短路径。算法具体的形式包括:

  • 确定起点的最短路径问题 - 也叫单源最短路问题,即已知起始结点,求最短路径的问题。在边权非负时适合使用Dijkstra算法,若边权为负时则适合使用Bellman-ford算法或者SPFA算法
  • 确定终点的最短路径问题 - 与确定起点的问题相反,该问题是已知终结结点,求最短路径的问题。在无向图中该问题与确定起点的问题完全等同,在有向图中该问题等同于把所有路径方向反转的确定起点的问题。
  • 确定起点终点的最短路径问题 - 即已知起点和终点,求两结点之间的最短路径。
  • 全局最短路径问题 - 也叫多源最短路问题,求图中所有的最短路径。适合使用Floyd-Warshall算法

用于解决最短路径问题的算法被称做“最短路径算法”,有时被简称作“路径算法”。最常用的路径算法有:

Dijkstra算法

戴克斯特拉算法(英语:Dijkstra’s algorithm),又译迪杰斯特拉算法,亦可不音译而称为Dijkstra算法,使用类似广度优先搜索的方法解决赋权图的单源最短路径问题。

戴克斯特拉的原始版本仅适用于找到两个顶点之间的最短路径,后来更常见的变体固定了一个顶点作为源结点然后找到该顶点到图中所有其它结点的最短路径,产生一个最短路径树。

该算法解决了图$G=(V,E)$上带权的单源最短路径问题。具体来说,戴克斯特拉算法设置了一顶点集合S,在集合S中所有的顶点与源点s之间的最终最短路径权值均已确定。算法反复选择最短路径估计最小的点u(属于集合V-S),并将u加入S中。该算法常用于路由算法或者作为其他图算法的一个子模块。举例来说,如果图中的顶点表示城市,而边上的权重表示城市间开车行经的距离,该算法可以用来找到两个城市之间的最短路径。

应当注意,绝大多数的戴克斯特拉算法不能有效处理带有负权边的图。

算法描述

将所有节点分成两类:已确定从起点到当前点的最短路长度的节点,以及未确定从起点到当前点的最短路长度的节点(下面简称「未确定节点」和「已确定节点」)。

每次从「未确定节点」中取一个与起点距离最短的点,将它归类为「已确定节点」,并用它「更新」从起点到其他所有「未确定节点」的距离。直到所有点都被归类为「已确定节点」。

用节点 A「更新」节点 B 的意思是,用起点到节点 A 的最短路长度加上从节点 A 到节点 B 的边的长度,去比较起点到节点 B 的最短路长度,如果前者小于后者,就用前者更新后者。这种操作也被叫做「松弛」。

1
2
3
4
5
6
7
8
9
10
11
function Dijkstra(G, w, s)
// 实际上的操作是将每个除原点外的顶点置为无穷大
// d[v] = INF, d[0] = 0
INITIALIZE-SINGLE-SOURCE(G, s)
S <- 空集
Q <- s // Q 是顶点V的优先队列,以顶点的最短路径估计排序
while(Q != 空集)
do u <- EXTRACT - MIN(Q) // 取出最小点
S <- u
for each vertex v 属于 Adj[u]
do RELAX(u, v, w) // 松弛成功的结点会被加入到队列中

示例-网络延迟时间

743. 网络延迟时间

有 n 个网络节点,标记为 1 到 n。

给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。

现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public int networkDelayTime(int[][] times, int n, int k) {
// 求出最长路径即可
// 距离为INF表示不可达,因为有相加的操作,故需要除2操作
int INF = Integer.MAX_VALUE / 2;
// 邻接矩阵
int[][] g = new int[n][n];
for (int i = 0; i < n; i ++) {
Arrays.fill(g[i], INF);
}
for (int[] time : times) {
int x = time[0] - 1, y = time[1] - 1;
g[x][y] = time[2];
}
// 当前点到源点的距离
int[] dist = new int[n];
Arrays.fill(dist, INF);
dist[k-1] = 0;
// 已确定
boolean[] used = new boolean[n];
// 遍历n次,每次找到一个距离最短的点
for (int i = 0; i < n; i ++) {
// 找到未确定点里的距离最短顶点
int x = -1;
for (int y = 0; y < n; y ++) {
if (!used[y] && (x == -1 || dist[y] < dist[x])) {
x = y;
}
}
// 标记最小距离点
used[x] = true;
// 松弛操作:更新邻接点
for (int y = 0; y < n; y ++) {
dist[y] = Math.min(dist[x] + g[x][y], dist[y]);
}
}

// 求出最大时间
int max = 0;
for (int d : dist) {
max = Math.max(d, max);
}
return max == INF ? -1 : max;
}

使用优先队列,降低时间复杂度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public int networkDelayTime(int[][] times, int n, int k) {
// Dijkstra
// 分为确定点和未确定点,每次去除最小未确定点,添加到确定点里并更新
int INF = Integer.MAX_VALUE / 2;
// 邻接表
ArrayList<int[]>[] g = new ArrayList[n];
for (int i = 0; i < n; i ++) {
g[i] = new ArrayList<>();
}
for (int[] time : times) {
int x = time[0] - 1;
int y = time[1] - 1;
g[x].add(new int[]{y, time[2]});
}
// 与k的距离
int[] dist = new int[n];
Arrays.fill(dist, INF);
dist[k-1] = 0;

// 未确定点
PriorityQueue<int[]> pq = new PriorityQueue<>(new Comparator<int[]>() {
public int compare(int[] a, int[] b) {
// 路径相等时,索引小的放在前面
return a[1] == b[1] ? a[0] - b[0] : a[1] - b[1];
}
});
pq.offer(new int[]{k-1, dist[k-1]});
while(!pq.isEmpty()) {
int[] p = pq.poll();
int x = p[0], time = p[1];
// 已经确定过了 确定过的距离比当前距离小
if (dist[x] < time) {
continue;
}
// 更新邻接点
for (int[] adj : g[x]) {
int y = adj[0], dy = time + adj[1];
if (dy < dist[y]) {
pq.offer(new int[]{y, dy});
dist[y] = dy;
}
}
}

// 求出最大值
int max = 0;
for (int d : dist) {
max = Math.max(max, d);
}
return max == INF ? -1 : max;
}

相关题目

Bellman–Ford算法

贝尔曼-福特算法(英语:Bellman–Ford algorithm),求解单源最短路径问题的一种算法,由理查德·贝尔曼(Richard Bellman) 和 莱斯特·福特 创立的。有时候这种算法也被称为 Moore-Bellman-Ford 算法,因为 Edward F. Moore 也为这个算法的发展做出了贡献。它的原理是对图进行 $|V|-1$次松弛操作,得到所有可能的最短路径。其优于迪科斯彻算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,高达$O(|V||E|)$。但算法可以进行若干种优化,提高了效率。

示例

  1. 网络延迟时间 有 n 个网络节点,标记为 1 到 n。 给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。 现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。

常规

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public int networkDelayTime(int[][] times, int n, int k) {
// Bellman-Ford 算法:以点拓展深度,以边拓展广度
int INF = Integer.MAX_VALUE / 2;
int[] dist = new int[n];
Arrays.fill(dist, INF);
dist[k-1] = 0;

// 拓展深度
for (int i = 0; i < n; i ++) {
// 记录上次的距离
int[] pre = dist.clone();
// 拓展广度
for (int[] time : times) {
int x = time[0]-1, y = time[1]-1, t = time[2];
dist[y] = Math.min(pre[x] + t, dist[y]);
}
}

// 求最大值
int max = 0;
for (int d : dist) {
max = Math.max(max, d);
}

return max == INF ? -1 : max;
}

队列优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public int networkDelayTime(int[][] times, int n, int k) {
// Bellman-Ford 队列优化:只遍历松弛过的点
// 初始化
int INF = Integer.MAX_VALUE / 2;
// 100个点,可以用邻接矩阵
int[][] g = new int[n][n];
for (int i = 0; i < n; i ++) {
Arrays.fill(g[i], INF);
}
for (int[] time : times) {
int x = time[0] - 1, y = time[1] - 1, w = time[2];
g[x][y] = w;
}
// 最短距离
int[] dist = new int[n];
Arrays.fill(dist, INF);
dist[k-1] = 0;
// 在队列中的点,无需再次入队
boolean[] inq = new boolean[n];

// 队列,松弛了且不在队列中的点,入队
Queue<Integer> q = new LinkedList<>();
q.offer(k-1);
inq[k-1] = true;
while (!q.isEmpty()) {
int x = q.poll();
inq[x] = false;
for (int y = 0; y < n; y ++) {
if (y == x) continue;
int dy = g[x][y] + dist[x];
if (dy < dist[y]) {
dist[y] = dy;
if (!inq[y]) {
q.offer(y);
inq[y] = true;
}
}
}
}

int max = -1;
for (int d : dist) {
max = Math.max(max, d);
}
return max == INF ? -1 : max;
}

单源最短路径适用算法

无权图

算法:广度优先搜索,时间复杂度O(E+V)

有向无环图

使用拓扑排序算法可以在有权值的DAG中以线性时间O(E+V) 求解单源最短路径问题。

无负权图

Dijkstra算法:设置了一顶点集合S,在集合S中所有的顶点与源点s之间的最终最短路径权值均已确定。算法反复选择最短路径估计最小的点u(属于集合V-S),并将u加入S中。

正负权图

Bellman-Ford 算法:对图进行 $|V|-1$次松弛操作,得到所有可能的最短路径。以点遍历深度,以线拓展广度。

多源最短路径

Floyd-Warshall算法。可以正确处理有向图或负权(但不可存在负权回路)的最短路径问题。

原理:使用动态规划,从i到j只以(1..k)集合中的节点为中间节点的最短路径,等于经过k和不经过k的最短路中的最小值。

伪代码:

1
2
3
4
5
6
7
8
9
10
11
let dist be a |V| × |V| array of minimum distances initialized to ∞ (infinity)
for each vertex v
dist[v][v] ← 0
for each edge (u,v)
dist[u][v] ← w(u,v) // the weight of the edge (u,v)
for k from 1 to |V|
for i from 1 to |V|
for j from 1 to |V|
if dist[i][j] > dist[i][k] + dist[k][j]
dist[i][j] ← dist[i][k] + dist[k][j]
end if

参考

简介

在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,才能称为该的一个拓扑排序(英语:Topological sorting):

  1. 序列中包含每个顶点,且每个顶点只出现一次;
  2. 若A在序列中排在B的前面,则在图中不存在从B到A的路径

当且仅当图中没有定向环时(即有向无环图),才有可能进行拓扑排序。有向无环图与拓扑排序互为充要条件。

两个推论:

  • 如果图G存在环,则图G不存在拓扑排序。
  • 如果图G是有向无环图,则可能存在多个拓扑排序。

求有向无环图的拓扑排序有两种方法:DFS 和 卡恩算法

阅读全文 »

简介

图(Grap)是用来对对象之间的成对关系建模的数学结构,由”节点”或”顶点”(Vertex)以及连接这些顶点的”边”(Edge)组成。

图包含一个有限(可能是可变的)的集合作为节点集合,以及一个无序对(对应无向图)或有序对(对应有向图)的集合作为边(有向图中也称作弧)的集合。

图的数据结构还可能包含和每条边相关联的数值(edge value),例如一个标号或一个数值(即权重,weight;表示花费、容量、长度等),表示加权图。

图的分类

  • 无向图和有向图。
  • 无权图和有权图。每条边是否有相关联的数值,有的话就是有权图,否则就是无权图。

图的连通性

在图论中,连通图基于连通的概念。在一个无向图 G 中,若从顶点 i 到顶点 j 有路径相连(当然从j到i也一定有路径),则称 i 和 j 是连通的。如果 G 是有向图,那么连接i和j的路径中所有的边都必须同向。如果图中任意两点都是连通的,那么图被称作连通图。如果此图是有向图,则称为强连通图(注意:需要双向都有路径)。图的连通性是图的基本性质。

完全图:完全是一个简单的无向图,其中每对不同的顶点之间都恰连有一条边相连。

自环边:一条边的起点终点是一个点。

平行边:两个顶点之间存在多条边相连接。

节点的度

无向图中,节点的度(英语:degree或valency)表示该节点作为一个端点的边的数目。

有向图中,分别为节点的入度和出度

  • 入度:(英语:in-degree)是指终点为该节点的边的数量。
  • 出度:(英语:out-degree)是指起点为该节点的边的数量。

路径和回路

  • 路径。无论是无向图还是有向图,从一个顶点到另一顶点途径的所有顶点组成的序列(包含这两个顶点),称为一条路径。若路径中各顶点都不重复,此路径又被称为”简单路径”。
  • 回路或环。如果路径中第一个顶点和最后一个顶点相同,则此路径称为”回路”(或”环”)。同样,若回路中的顶点互不重复,此回路被称为”简单回路”(或简单环)。
  • 路径长度。是指一条路径上经过的边的数量。
阅读全文 »

简介

并查集(英文:Disjoint-set data structure,直译为不交集数据结构),用于处理一些不交集(Disjoint sets,一系列没有重复元素的集合)的合并及查询问题。并查集支持如下操作:

  • 查询:查询某个元素属于哪个集合,通常是返回集合内的一个“代表元素”(一般为根节点)。这个操作是为了判断两个元素是否在同一个集合之中。
  • 合并:将两个集合合并为一个。
  • 添加:添加一个新集合,其中有一个新元素。添加操作不如查询和合并操作重要,常常被忽略

由于支持查询和合并这两种操作,并查集在英文中也被称为联合-查找数据结构(Union-find data structure)或者合并-查找集合(Merge-find set)。

一般来说,“并查集”特指其中最常见的一种实现:不交集森林(Disjoint-set forest)。经过优化的不交集森林有线性的空间复杂度,以及接近常数的单次操作平均时间复杂度,是效率最高的常见数据结构之一。

并查集是用于计算最小生成树的克鲁斯克尔算法中的关键。由于最小生成树在网络路由等场景下十分重要,并查集也得到了广泛的引用。此外,并查集在符号计算,寄存器分配等方面也有应用。

具体适用场景:凡是涉及到元素的分组管理问题,都可以考虑使用并查集进行维护。

  • 维护无向图的连通性:支持判断两个点是否在同一连通块内。
  • 判断增加一条边是否会产生环:用在求解最小生成树的Kruskal算法里。

不交集森林

表示

不交集森林把每一个集合以一棵树表示,每一个节点即是一个元素。节点保存着到它的父节点的引用,树的根节点则保存一个空引用或者到自身的引用或者其他无效值,以表示自身为根节点。

添加(初始化)

添加操作MakeSet(x)添加一个元素x,这个元素单独属于一个仅有它自己的集合。在不交集森林中,添加操作仅需将元素标记为根节点。用伪代码表示如下:

1
2
3
function MakeSet(x)
x.parent := x
end function

在经过优化的不交集森林中,添加操作还会初始化一些有关节点的信息,例如集合的大小或者秩。

查询(带路径压缩优化)

在不交集森林中,每个集合的代表即是集合的根节点。查询操作Find(x)x开始,根据节点到父节点的引用向根行进,直到找到根节点。用伪代码表示如下:

1
2
3
4
5
6
7
function Find(x)
if x.parent = x then
return x
else
return Find(x.parent)
end if
end function

路径压缩优化

在集合很大或者树很不平衡时,上述代码的效率很差,最坏情况下(树退化成一条链时),单次查询的时间复杂度高达O(n)。一个常见的优化是路径压缩:在查询时,把被查询的节点到根节点的路径上的所有节点的父节点设置为根结点,从而减小树的高度。也就是说,在向上查询的同时,把在路径上的每个节点都直接连接到根上,以后查询时就能直接查询到根节点。注意,这仅仅只是压缩了某一个路径上的节点。用伪代码表示如下:

1
2
3
4
5
6
7
8
function Find(x)
if x.parent = x then
return x
else
x.parent := Find(x.parent)
return x.parent
end if
end function

合并(按大小或按秩合并)

合并操作Union(x, y)把元素x所在的集合与元素y所在的集合合并为一个。合并操作首先找出节点x与节点y对应的两个根节点,如果两个根节点其实是同一个,则说明元素x与元素y已经位于同一个集合中,否则,使其中一个根节点成为另一个的父节点。

1
2
3
4
5
6
7
8
9
// 普通合并
function Union(x, y)
xRoot := Find(x)
yRoot := Find(y)

if xRoot ≠ yRoot then
xRoot.parent := yRoot
end if
end function

按秩合并优化

上述代码的问题在于,可能会使得树不平衡,增大树的深度,从而增加查询的耗时。一个控制树的深度的办法是,在合并时,比较两棵树的大小,较大的一棵树的根节点成为合并后的树的根节点,较小的一棵树的根节点则成为前者的子节点。判断树的大小有两种常用的方法,一个是以树中元素的数量作为树的大小,这被称为按大小合并。另一种做法则是使用“秩”来比较树的大小。”秩“的定义如下:

  • 只有根节点的树(即只有一个元素的集合),秩为0;
  • 当两棵秩不同的树合并后,新的树的秩为原来两棵树的秩的较大者;
  • 当两棵秩相同的树合并后,新的树的秩为原来的树的秩加一。

容易发现,在没有路径压缩优化时,树的秩等于树的深度减一。在有路径压缩优化时,树的秩仍然能反映出树的深度和大小。在合并时根据两棵树的秩的大小,决定新的根节点,这被称作按秩合并。用伪代码表示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function MakeSet(x)
x.parent := x
x.rank := 0
end function

function Union(x, y)
xRoot := Find(x)
yRoot := Find(y)

if xRoot ≠ yRoot then
if xRoot.rank < yRoot.rank then
large := yRoot
small := xRoot
else
large := xRoot
small := yRoot
end if

small.parent := large
if large.rank = small.rank then
large.rank := large.rank + 1
end if
end if
end function

只有根节点的rank有意义,非根节点的rank是没有意义的。

时间及空间复杂度

空间复杂度:容易看出,不交集森林的空间复杂度是$O(n)

空间复杂度:对于同时使用路径压缩和按秩合并优化的不交集森林,每个查询和合并操作的平均时间复杂度仅为O(\alpha(n)),$\alpha(n)$是反阿克曼函数。由于阿克曼函数$A$增加极度迅速,所以$\alpha(n)$增长极度缓慢,对于任何在实践中有意义的元素数目n,$\alpha(n)$均小于5,因此,也可以粗略地认为,并查集的操作有常数的时间复杂度。

示例-省份数量

547. 省份数量

有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。

省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。

给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。

返回矩阵中 省份 的数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Solution {
public int findCircleNum(int[][] isConnected) {
int n = isConnected.length;
// 初始化
UnionFind uf = new UnionFind(n);
// 遍历两个点之间的关系,做处理
for (int i = 0; i < n; i ++) {
for (int j = i + 1; j < n; j ++) {
if (isConnected[i][j] == 1) {
uf.union(i, j);
}
}
}

return uf.size;
}
}

// 并查集对象
class UnionFind {
// 每个节点对应的父节点
int[] parent;
// 不交集个数
int size;

public UnionFind(int n) {
// 初始化:n个不交集
parent = new int[n];
for (int i = 0; i < n; i ++) {
parent[i] = i;
}
size = n;
}

// 查找:带路径压缩优化
public int find(int index) {
if (parent[index] != index) {
parent[index] = find(parent[index]);
}
return parent[index];
}

// 普通合并
public void union(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot != qRoot) {
parent[pRoot] = qRoot;
size --;
}
}
}

示例:连通网络的操作次数

1319. 连通网络的操作次数

用以太网线缆将 n 台计算机连接成一个网络,计算机的编号从 0 到 n-1。线缆用 connections 表示,其中 connections[i] = [a, b] 连接了计算机 a 和 b。

网络中的任何一台计算机都可以通过网络直接或者间接访问同一个网络中其他任意一台计算机。

给你这个计算机网络的初始布线 connections,你可以拔开任意两台直连计算机之间的线缆,并用它连接一对未直连的计算机。请你计算并返回使所有计算机都连通所需的最少操作次数。如果不可能,则返回 -1 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class Solution {
public int makeConnected(int n, int[][] connections) {
// 并查集
// 连接数小于n-1的必定不能链接,返回-1
// 连接数大约等于n-1的必定能链接,链接操作数等于不交集个数-1
// 遍历连接数,合并
int m = connections.length;
if (m < n - 1) {
return -1;
}
UnionFind nf = new UnionFind(n);
for (int i = 0; i < m; i ++) {
nf.union(connections[i][0], connections[i][1]);
}

return nf.size - 1;
}


}

class UnionFind {
// 数组值为父节点
int[] parent;
// 秩
int[] rank;
// 不交集个数
int size;

public UnionFind(int n) {
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; i ++) {
parent[i] = i;
rank[i] = 0;
}
size = n;
}

public int find(int x) {
if (parent[x] == x) {
return x;
} else {
parent[x] = find(parent[x]);
return parent[x];
}
}

public void union(int x, int y) {
int xRoot = find(x);
int yRoot = find(y);
if (xRoot == yRoot) {
return;
}
if (rank[xRoot] < rank[yRoot]) {
parent[xRoot] = yRoot;
} else if (rank[xRoot] > rank[yRoot]){
parent[yRoot] = xRoot;
} else if (rank[xRoot] == rank[yRoot]) {
parent[yRoot] = xRoot;
rank[xRoot] = rank[xRoot] + 1;
}
size --;
}
}

相关题目

参考

树形结构包括:线段树、自平衡二叉查找树、B树、二叉树、AA树、AVL树、红黑树、平衡树、伸展树、二叉查找树、堆、二叉堆、左偏树、二项堆、斐波那契堆、R树、R*树、R+树、Hilbert R树、前缀树、哈希树、图等等。本章介绍最基本的二叉树,其他所有的树都能转变为二叉树处理。

阅读全文 »

简介

数据结构

数据结构的存储方式只有两种:数组(顺序存储)和链表(链式存储),其他的都可以用这两者来实现,比如队列、栈、图、散列表、树等。

数据结构的遍历和访问:线性方式即迭代,非线性方式即递归(或队列栈加迭代)。

算法

核心在于穷举,然后剪枝。剪枝可利用记录表。

阅读全文 »

最近得空,记录下这几月Android开发中遇到的坑。

这篇文章主要记录集成腾讯浏览服务(TBS Tencent Browse Server)时遇到的问题以及解决方案。

阅读全文 »

脚本和对象

构建时,脚本文件会转化为对象;写脚本时不懂的地方查对应对象API即可。

  • settings.gradle会转化为Settings对象。
  • build.gradle会转化为Project对象。
  • 其他gradle文件,没有定义为class的,会转换为一个实现了Script接口的对象
阅读全文 »