c++基础数据结构

基础数据结构
目录
• 线性结构
• 二叉堆
• 并查集
• 哈希表
• 应用举例 一、线性结构 基础知识
• 数组
• 带头结点的双链表
-- He a d 结点 : 虚拟头结点
-- Fir s t 结点 : 第一个有实际内容的结点
• 队列 : 循环队列与 Open-Close 表 例 1. 最小值
• 实现一个 n 个元素的线性表 A 。每次可以修
改其中一个元素,也可以询问闭区间 [p, q]
中元素的最小值。
• 1<=n,m<=100000 分析
• 利用二级检索的思想
-- 设块长为 L, 则一共有 n/L 个块
-- 维护数组 B, 保存每个块的最小值
• Modify(x, y)
-- A[x] = y O(1)
-- 重算 x 所在块的最小值 ( 更新 B) O(L)
9 10 11 12 13 14 15 16
8
7
6
5
4
3
1 2
13~16
9~12
5~8
1~4 Min 操作
• Min(a, b)
-- 把区间 [a, b] 分成若干部分
-- 完整块 : 一共最多 n/L 个块
O(n/L)
-- 非完整块 : 首尾各最多 L-1 个元素
O(L)
• 每次操作时间复杂度 : O(n/L+L)
-- 设 L=O(n 1/2 ) 则渐进时间复杂度为 O(n 1/2 )
√ √ √


√ 例 2. 最接近的值
• 给一个 n 个元素的线性表 A ,对于每个数 A i
找到它之前的数中,和它最接近的数。即
对于每个 i ,计算
C i =min{| A i - A j | | 1<= j < i }
• 规定 C 1 =0 。 分析
• 问题的关键 : 你只需要解决 离线 问题
-- 在线问题 : 每输入一个 A i , 立刻输出 C i
-- 离线问题 : 在给出任何输出之前得到所有 A i
• 预处理 : 用 O(nlogn) 时间给所有 A i 排序
-- 很快就会学到了 ☺
• 主算法
-- 根据从小到大的顺序构造双向链表
-- 依次计算 C n , C n-1 , ..., C 1 在线问题可能么 ? 主算法
• A={2, 6, 5, 10, 4, 7}, 依次计算 C 6 , C 5 , C 4
-- 每次计算 C i 时 , 链表中恰好是 计算 C i 需要的元素
-- 计算 C i 只需比较两个元素,然后删除 A i
O(1)
2 2
4 4
5 5
6 6
7 7
10 10
2 2
4 4
5 5
6 6
10 10
2 2
5 5
6 6
10 10 例 3. 移动窗口
• 有一个长度为 n 的数组,还有一个长度为
k<=n 的窗口。窗口一开始在最左边的位
置,能看到元素 1, 2, 3, ...k 。每次把窗口往
右移动一个元素的位置,直到窗口的右边
界到达数组的元素 n 。 分析
• 考虑最小值。假设窗口从左往右移动
• 保存 5 是不明智的 , 因为从 现在 开始一直到 5
离开窗口 , 5 始终被 4" 压制 " ,永远都不可能
成为最小值。删除 5 不会影响 结果
启发 :算最小值的 有用 元素形成一个递增
序列,最小值就是队首元素
关键: 窗口右移操作
...
4
5
3
... 2 分析
窗口右移: 队首出队,新元素入队,然后
在队列中删除它前面比它大的元素
• 实现
-- 用链表储存窗口内 有用 元素
-- 则窗口右移的时间和删除元素的 次数成正比
• 元素被删除后不会再次被插入,因此每个
元素最多被删除一次,总次数为 O(n), 即:
算法时间总复杂度为 O(n)
...
4
12
11
9
7
5
3
3
... 例 4. 程序复杂度
• 给出一段程序,计算它的时间复杂度。这段程序
只有三种语句:
-- OP <x > :执行一条时间耗费为 x 的指令。这里的 x 为不
超过 100 的正整数。
-- LOOP <x> :与 END 配套,中间的语句循环 x 次,其中
x 可以为规模 n ,也可以是不超过 100 的正整数。
-- EN D :与 LOOP 配套,循环终止。
• 注意: OP 语句的参数不能为变量 n ,而 LOOP 语
句的参数可以为变量 n ,但不允许有其他名字的变
量。这样,整个程序的时间复杂度应为 n 的多项
式。你的任务就是计算并显示出这个多项式。 例 4. 程序复杂度
• 输出仅包含一行,即时间复杂度的多项
式。这个多项式应该按通常的方式显示处
理,即不要输出 0n 这样的项, n 项也不要输
出为 n^1 ,等等。
OP 1
LOOP n
LOOP 10
LOOP n OP 7 END
OP 1
END
END
70n^2+10n+1 分析
• 考虑特殊情况:没有变量 n 只有常数的情况
• 两种思路
-- 递归求解:不直接,附加开销大 /
-- 基于栈的扫描算法:实现简单明了,效率高 ☺
• 扫描过程(基本想法)
-- LO O P x 和 OP x, 把语句入栈
-- END, 不断从栈里弹出语句,直到弹出 LOOP
• 弹出过程中累加操作数 x ,然后乘以循环次数 y
• 把 OP x*y 压入栈中,相当于用一条 OP 等效一个 LOOP 分析
• 栈扫描算法的直接推广
-- 栈里的每个操作数不是数,而是多项式
-- 多项式加法,多项式与整数的乘积
-- 所有通过至少一个数据的同学都采用此法
• 问题
-- 多项式如何表示
-- 多项式操作的时间复杂度是怎样的? 复杂性
• 大部分数据涉及到高精度整数运算
• 高精度运算的复杂性
-- 写起来相对麻烦
-- 时间复杂度: O(n 2 ) (nlogn is possible, but...)
-- 空间复杂度
• 实际情况:所有使用了高精度运算的同学
在时间 " 爆 " 掉之前空间先 " 爆 " 掉
-- 每个数 10000 位 , n 次数可达 10000 (或更多)
-- 栈里面可以有 10000 个多项式, 10 12 =1T !!!!! 空间 ... 空间 ... 空间 ...
• 是否可以找到一个空间需求比较小的算
法? 哪怕时间效率略一点都可以
• 输出文件已经不小了,需要尽量少的保存
中间结果,因此最好不要栈!
-- 只要有栈,最坏情况中间结果的空间大小就是
输出大小乘以栈的最大深度,而输出 ... " 把括号展开 "
• 先想办法去掉所有 LOOP/END
• 借助 当前乘数 确定这次 OP 的 最终 效果
OP 1
LOOP n
LOOP 10
LOOP n
OP 7
END
OP 1
END
END
OP 1 * 1
OP (n*10*n) * 7
OP (n*10) * 1 基本算法
• 设当前乘数为 m, 结果多项式为 ans
-- 初始化 m= 1, ans = 0
• 主循环:一条一条语句处理
-- 遇到 LOOP x , m = m * x
-- 遇到 END , m = m / x
-- 遇到 OP x, ans = ans + m * x
• 数据结构:设 m = an b ,则用数对 (a, b) 表示
m , a 是高精度整数, b 是 int 类型
• 除了结果多项式 ans 和 a , 其他空间可忽略 二、二叉堆
• 堆 (heap) 经常被用来实现优先队列 (priority
queue): 可以把元素加入到优先队列中 , 也
可以从队列中取出 优先级最高 的元素
-- Insert(T, x): 把 x 加入优先队列中
-- DeleteMin(T, x): 获取优先级最高的元素 x, 并
把它从优先队列中删除 堆的操作
• 除了实现优先队列 , 堆还有其他用途 , 因此
操作比优先队列多
-- Getmin(T, x): 获得最小值
-- Delete(T, x): 删除任意已知结点
-- DecreaseKey(T, x, p): 把 x 的优先级降为 p
-- Build(T, x): 把数组 x 建立成最小堆
• 显然 , 用堆很容易实现优先队列 堆的定义
• 堆是一个完全二叉树
-- 所有叶子在同一层或者两个连续层
-- 最后一层的结点占据尽量左的位置
• 堆性质
-- 为空 , 或者最小元素在根上
-- 两棵子树也是堆 存储方式
• 最小堆的元素保存在 heap[1..hs] 内
-- 根在 heap[1]
-- K 的左儿子是 2k, K 的右儿子是 2k+1,
-- K 的父亲是 [k/2]
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
10 10
11 11 12 12
13 13 14 14 删除最小值元素
• 三步法
-- 直接删除根
-- 用最后一个元素代替根上元素
-- 向下调整
13 13
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9 10 10
11 11 12 12
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9 10 10
11 11 12 12
13 13 向下调整
• 首先选取当前结点 p 的较大儿子 . 如果比 p 大 , 调整
停止 , 否则交换 p 和儿子 , 继续调整
13 13
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9 10 10
11 11 12 12
2 2
13 13
3 3
4 4
5 5
6 6
7 7
8 8
9 9 10 10
11 11 12 12
void swim(int p){
int q = p>>1, a = heap[p];
while(q && a<heap[q]){ heap[p]=heap[q]; p=q; q=p>>1; }
heap[p] = a;
} 插入元素和向上调整
• 插入元素是先添加到末尾 , 再 向上调整
• 向上调整 : 比较当前结点 p 和父亲 , 如果父亲比 p
小,停止 ; 否则交换父亲和 p, 继续调整
void sink(int p){
int q=p<<1, a = heap[p];
while(q<=hs){
if(q<hs&&heap[q+1]<heap[q])q++;
if(heap[q]>=a) break;
heap[p]=heap[q]; p=q; q=p<<1;
}
heap[p] = a;
} 堆的建立
• 从 下往上逐层 向下调整 . 所有的叶子无需调整 ,
因此从 hs/2 开始 . 可用数学归纳法证明循环变量
为 i 时 , 第 i+1, i+2, ...n 均为最小堆的根
void insert(int a)
{ heap[++hs]=a; swim(hs); }
int getmin()
{ int r=heap[1]; heap[1]=heap=[hs--];
sink(1); return r; }
int decreaseKey(int p, int a )
{ heap[p]=a; swim(p); }
void build()
{ for(int i=hs/2;i>0;i--) sink(i); } 时间复杂度分析
• 向上调整 / 向下调整
-- 每层是常数级别 , 共 logn 层 , 因此 O(logn)
• 插入 / 删除
-- 只调用一次向上或向下调整 , 因此都是 O(logn)
• 建堆
-- 高度为 h 的结点有 n/2 h+1 个 , 总时间为
lo
g
l o g
1
0 0
( )
2 2
n n
h h
h h
n h
O h O
n
⎢ ⎥
⎢ ⎥
⎣ ⎦
⎣ ⎦
+
= =
⎛ ⎞
⎡ ⎤
× =
⎜ ⎟
⎢ ⎥
⎜ ⎟
⎢ ⎥
⎝ ⎠
∑ ∑ 例 1. k 路归并问题
• 把 k 个有序表合并成一个有序表 .
• 元素共有 n 个 . 分析
• 每个表的元素都是从左到右移入新表
• 把每个表的当前元素放入二叉堆中 , 每次删
除最小值并放入新表中 , 然后加入此序列的
下一个元素
• 每次操作需要 logk 时间 , 因此总共需要 nlogk
的时间 例 2. 序列和的前 n 小元素
• 给出两个长度为 n 的有序表 A 和 B, 在 A 和 B 中
各任取一个 , 可以得到 n 2 个和 . 求这些和最
小的 n 个 分析
• 可以把这些和看成 n 个有序表 :
-- A[1]+B[1] <= A[1]+B[2] <= A[1]+B[3] <=...
-- A[2]+B[1] <= A[2]+B[2] <= A[2]+B[3] <=...
-- ...
-- A[n]+B[1] <= A[n]+B[2] <= A[n]+B[3] <=...
• 类似刚才的算法 , 每次 O(logn), 共取 n 次最
小元素 , 共 O(nlogn) 例 3. 轮廓线
• 每一个建筑物用一个三元组表示 (L, H, R), 表示
左边界 , 高度和右边界
• 轮廓线用 X, Y, X, Y... 这样的交替式表示
• 右图的轮廓线为 : (1, 11, 3, 13, 9, 0, 12, 7, 16,
3, 19, 18, 22, 3, 23, 13, 29, 0)
• 给 N 个建筑,求轮廓线 分析
• 算法一 : 用数组记录每一个元线段的高度
-- 离散化 , 有 n 个元线段
-- 每次插入可能影响 n 个元线段 , O(n), 共 O(n 2 )
-- 从左到右扫描元线段高度 , 得轮廓线
• 算法二:每个建筑的左右边界为事件点
-- 把事件点排序 , 从左到右扫描
-- 维护建筑物集合 , 事件点为线段的插入删除
-- 需要求最高建筑物 , 用堆 , 共 O(nlogn) 例 4. 丑数
• 素因子都在集合 {2, 3, 5, 7} 的数称为 ugly
number
• 求第 n 大的丑数 分析
• 初始:把 1 放入优先队列中
• 每次从优先队列中取出一个元素 k ,把 2k,
3k, 5k, 7k 放入优先队列中
• 从 2 开始算,取出的第 n 个元素就是第 n 大的
丑数
• 每取出一个数,插入 4 个数,因此任何堆里
的元素是 O(n) 的,时间复杂度为 O(nlogn) 例 5. 赛车
• 有 n 辆赛车从各不相同的地方以各种的速度 ( 速度
0<v i <100) 开始往右行驶,不断有超车现象发生。
• 给出 n 辆赛车的描述(位置 x i ,速度 v i ),赛车已
按照位置排序(
x 1 <x 2 <...<x n )
• 输出超车总数以及按时间顺序的前 m 个超车事件
V 3
V 4
V 1
V 2
X 2 X 3
X 4
X 1 分析
• 事件个数 O(n 2 ), 因此只能一个一个求
• 给定两辆车,超越时刻预先可算出
第一次 超车 可能 在哪些辆车之间?
-- 维护所有车的前方相邻车和追上时刻
• 局部:此时刻不一定是该车下个超车时刻!
• 全局:所有时刻的 最小值 就是下次真实超车时刻
• 维护:超车以后有什么变化?
-- 相对顺序变化 ... 改变三个车的前方相邻车
-- 重新算追上时刻,调整三个权
-- 简单的处理方法:删除三个再插入三个 例 6. 可怜的奶牛
• 农夫 John 有 n
n ≤ 100 000 )头奶牛,可是由于
它们产的奶太少,农夫对它们很不满意,决定每
天把产奶最少的一头做成牛肉干吃掉。但还是有
一点舍不得, John 打算如果不止有一头奶牛产奶
最少,当天就大发慈悲,放过所有的牛。
• 由于 John 的奶牛产奶是周期性的, John 在一开始
就能可以了解所有牛的最终命运,不过他的数学
很差,所以请你帮帮忙,算算最后有多少头奶牛
可以幸免于难。每头奶牛的产奶周期 T i 可能不
同,但不会超过 10 。在每个周期中,奶牛每天产
奶量不超过 200 。 分析
• 如果采用最笨的方法,每次先求出每头牛的产奶
量,再求最小值,则每天的复杂度为 O(n) ,总复
杂度为 O(Tn) ,其中 T 是模拟的总天数。由于周期
不超过 10 ,如果有的牛永远也不会被吃掉,那么
我们需要多模拟 2520 天( 1 , 2 , 3 , ... , 10 的最
小公倍数)才能确定
• 周期同为 t 的奶牛在没有都被吃掉之前,每天的最
小产奶量也是以 t 为周期的。因此如果把周期相同
的奶牛合并起来,每天只需要比较 10 类奶牛中每
类牛的 最小产奶量就可以了,每天的复杂度为
O(k) ,其中 k 为最长周期 分析
• 假设周期为 6 的牛有 4 头,每次只需要比较 k
组牛的 " 代表 " 就可以了,每天模拟的时间复
杂度为 O ( k ) 。


第 6 n + 1 天 第 6 n + 2 天 第 6 n + 3 天 第 6 n + 4 天 第 6 n + 5 天 第 6 n + 6 天
牛 1
2
5
3
5
7
4
牛 2
3
1
6
7
5
4
牛 3
5
3
3
5
3
9
牛 4
4
4
3
8
8
2
合 并 结 果
2 ( 牛 1 )
1 ( 牛 2 )
3 ( 多 )
5 ( 多 )
3 ( 牛 3 )
2 ( 牛 4 ) 分析
• 只要周期为 6 的牛都不被吃掉, 这个表一直是有效
。但是在吃掉一头奶牛后,我们需要修改这个
表,使它仍然记录着每天的最小产奶量
-- 方法一 : 重新计算,时间 O(h) ,其中 h 是该组的牛数
-- 方法二 : 把 一个周期中每天 的最小产奶量组织成堆,每
次删除操作的复杂度是 O(klogh)
• 由于每头奶牛最多被吃掉一次,因此用在维护 " 最
小产奶量结构 " 的总复杂度不超过 O(nklogn) 。每
天复杂度为 O(k) ,总复杂度为 O(Tk+nklogn) 例 7. 黑匣子
• 我们使用黑匣子的一个简单模型。它能存
放一个整数序列和一个特别的变量 i 。在初
始时刻,黑匣子为空且 i 等于 0 。这个黑匣子
执行一序列的命令。有两类命令:
• AD D ( x ) :把元素 x 放入黑匣子;
• GE T : i 增 1 的同时,输出黑匣子内所有整数
中第 i 小的数。牢记第 i 小的数是当黑匣子中
的元素以非降序排序后位于第 i 位的元素 例 7. 黑匣子
编 号
命 令
i
黑 匣 子 内 容
输 出
1
A D D ( 3 )
0
3
2
G E T
1
3
3
3
A D D ( 1 )
1
1, 3
4
G E T
2
1, 3
3
5
A D D ( - 4 )
2

  • 4, 1, 3
    6
    A D D ( 2 )
    2
  • 4, 1, 2, 3
    7
    A D D ( 8 )
    2
  • 4, 1, 2, 3, 8
    8
    A D D ( - 1 0 0 0 )
    2
  • 1 0 0 0, - 4, 1, 2, 3, 8
    9
    G E T
    3
  • 1 0 0 0, - 4, 1 , 2, 3, 8
    1
    1 0
    G E T
    4
  • 1 0 0 0, - 4, 1, 2 , 3, 8
    2
    1 1
    A D D ( 2 )
    4
  • 1 0 0 0, - 4, 1, 2, 2, 3, 8 分析
    • 降序堆 H ≥ 和升序堆 H ≤ 如图放置
    • H ≥ 根节点的值 H ≥ [1] 在堆 H ≥ 中最大,
    H ≤ 根节点的值 H ≤ [1] 在堆 H ≤ 中最小 ,
    并满足
    -- H ≥ [1] ≤ H ≤ [1]
    -- siz e [ H ≥ ]= i
    • ADD( x ): 比较 x 与 H ≥ [1] ,若 x
    H ≥ [1] ,则将 x 插入 H ≤ ,否则从 H ≥ 中
    取出 H ≥ [1] 插入 H ≤ ,再将 x 插入 H ≥
    • GE T: H ≤ [1] 就是待获取的对象。输
    出 H ≤ [1] ,同时从 H ≤ 中取出 H ≤ [1] 插入
    H ≥ ,以维护条件 (2) 三、并查集 并查集
    • 并查集维护一些不相交集合 S={S 1 , S 2 , ...,
    S r }, 每个集合 S r 都有一个特殊元素 rep[S i ],
    称为集合代表 . 并查集支持三种操作
    -- Make-Set(x): 加入一个集合 {x} 到 S, 且 rep[{x}]
    = x. 注意 , x 不能被包含在任何一个 S i 中 , 因为 S
    里任何两个集合应是不相交的
    -- Union(x, y): 把 x 和 y 所在的两个不同集合合并 .
    相当于从 S 中删除 S x 和 S y 并加入 S x US y
    -- Find-Set(x): 返回 x 所在集合 S x 的代表 rep[S x ] 链结构
    • 每个集合用双向链表表示 , rep[S i ] 在链表首部
    Make-Set(x): 显然是 O(1) 的
    Find-Set(x): 需要不断往左移 , 直到移动到首部 .
    最坏情况下是 O(n) 的
    Union(x, y): 把 S y 接在 S x 的尾部 , 代表仍是
    rep[S x ]. 为了查找链表尾部 , 需要 O(n) 增强型链结构
    • 给每个结点增加一个指回 rep 的指针
    Make-Set(x): 仍为常数
    Find-Set(x): 降为常数 ( 直接读 rep)
    Union(x, y): 变得复杂 : 需要把 S y 里所有元素的 rep
    指针设为 rep[Sx]! 增强型链结构的合并
    • 可以把 x 合并到 y 中,也可以把 y 合并在 x 中 技巧 1: 小的合并到大的中
    • 显然 , 把小的合并到大的中 , 这一次 Union 操
    作会比较节省时间 , 更精确的分析 ?
    • 用 n, m, f 分别表示 Make-Set 的次数 , 总操作
    次数和 Find-Set 的次数 , 则有
    定理 : 所有 Union 的总时间为 O(nlogn)
    推论 : 所有时间为 O(m + nlogn)
    证明 : 单独考虑每个元素 x, 设所在集合为 S x ,
    则修改 rep[x] 时 , S x 至少加倍 . 由于 S x 不超过
    n, 因此修改次数不超过 log 2 n, 总 nlogn 树结构
    • 每个集合用一棵树表示 , 根为集合代表 树结构的合并
    • 和链结构类似 , 小的合并到大的中 技巧 2: 路径压缩
    • 查找结束后顺便把父亲
    设置为根 , 相当于有选择
    的设置 rep 指针而不像链
    结构中强制更新 所有 rep 路径压缩的分析
    • 设 w[x] 为 x 的子树的结点数 , 定义势能函数
    Union(x i , x j ) 增加势能 . 最多会让 w[rep[xi]] 增加
    w[rep[xj]]<=n, 因此势能增加不超过 logn
    Find-Set(x) 减少势能 . 把路径压缩看作是从根到
    结点 x 的向下走过程 , 则除了第一次外的其他向下
    走的步骤 p Æ c 会让 c 的子树从 p 的子树中移出 , 即
    w[p] 减少 w[c], 而其他点的 w 值保持不变 路径压缩的分析
    • Find-Set 除了第一次外的其他向下走的步骤
    p Æ c 会让 c 的子树从 p 的子树中移出
    -- 情况一 : w[c]>=w[p]/2, 则势能将至少减少 1
    -- 情况二 : w[c]<w[p]/2, 这种情况最多出现 logn 次 ,
    因为 w[p] 最多进行 logn 次除 2 操作就会得到 1
    • Union 操作积累起来的 mlogn 的势能将被
    Find-Set 消耗 , 情况一最多消耗 mlogn 次 , 情
    况二本身不超过 mlogn 次 , 因此
    定理 : Find-Set 的总时间为 O(mlogn) 路径压缩的分析
    定理 : 如果所有 Union 发生在 Find-Set 之前 ,
    则所有操作的时间复杂度为 O(m)
    证明 : 每次 Find-Set 将会让路径上除了根的
    所有结点为根的儿子 . 所有结点只会有一次
    改变,因此总时间复杂度为 O(m)
    • 也就是说
    -- 只使用技巧 1( 启发式合并 ): O(m+nlogn)
    -- 只使用技巧 2( 路径压缩 ): O(mlogn)
    • 同时使用呢 ? Ackermann 函数及其反函数 树结构的完整结论
    定理 : m 个操作的总时间复杂度为 ( (
    ) )
    O m
    α n
    void makeset(int x){ rank[x] = 0; p[x]=x; }
    int findset(int x){
    int i, px = x;
    while (px != p[px]) px = p[px];
    while (x != px) { i = p[x]; p[x] = px; x = i; }
    return px;
    }
    void unionset (int x , int y){
    x = findset(x); y = findset(y);
    if(rank[x] > rank[y]) p[y] = x;
    else { p[x] = y; if(rank[x] == rank[y]) rank[y]++; }
    } 例 1. 亲戚
    • 或许你并不知道,你的某个朋友是你的亲戚。他
    可能是你的曾祖父的外公的女婿的外甥女的表姐
    的孙子。如果能得到完整的家谱,判断两个人是
    否亲戚应该是可行的,但如果两个人的最近公共
    祖先与他们像个好几代,使得家谱十分庞大,那
    么检验亲戚关系实非人力所能及。在这种情况
    下,最好的帮手就是计算机。
    • 为了将问题简化,你将得到一些亲戚关系的信
    息,如同 Marry 和 Tom 是亲戚, Tom 和 Ben 是亲
    戚,等等。从这些信息中,你可以推出 Marry 和
    Ben 是亲戚。请写一个程序,对于我们的关于亲
    戚关系的提问,以最快的速度给出答案。 分析
    • 本质 : 是否在图的同一个连通块
    • 问题 : 图太庞大 , 每次还需要遍历
    • 解决 : 用并查集 例 2. 矩形
    • 在平面上画了 N 个长方形,每个长方形的边平行
    于坐标轴并且顶点坐标为整数。我们用以下方式
    定义印版:
    -- 每个长方形是一个印版;
    -- 如果两个印版有公共的边或内部,那么它们组成新的
    印版,否则这些印版是分离的
    • 数出印版的个数 . 左图有两个,右图只有一个 分析
    • 把矩形看作点,有公共边的矩形连边,问
    题转化为求连通分量的个数
    • 判断方法:
    Y
    ( X 2 , Y 2 )
    )
    ,
    (
    2
    2

    Y
    X
    ( X 1 , Y 1 )
    )
    ,
    (
    1
    1

    Y
    X
    X 例 3. 代码等式
    • 由元素 0 和 1 组成的非空的序列称为一个二进制代
    码。一个代码等式就是形如 x 1 x 2 .. x l = y 1 y 2 .. y r , 这里 x i
    y j 是二进制的数字(
    0 或 1 )或者是一个变量(如
    英语中的小写字母)
    • 每一个变量都是一个有固定长度的二进制代码,
    它可以在代码等式中取代变量的位置。我们称这
    个长度为变量的长度
    • 对于每一个给出的等式,计算一共有多少组解。
    • 例 : a,b,c,d,e 的长度分别是 4,2,4,4,2, 则 1bad1 = acbe
    有 16 组解 分析
    • 长度为 k 的变量拆成 k 个长度为 1 的变量
    • 每位得到一个等式
    -- 1=1 或者 0=0 :冗余等式
    -- 1=0 或者 0=1 :无解
    -- a=b : a 和 b 相等(
    a 为变量 b 可以为常数)
    • 相等关系用并查集处理,最后统计集合数
    为 n ,答案为 2 n 。 例 4. 围墙
    • 按顺序给出 M 个整点组成的线段,找到最小
    的 k ,使得前 k 条线段构成了封闭图形。
    (任意两条线段只可能在端点相交) 分析
    • 将所有出现过的坐标用整数表示,初始时
    候每个独立成树。读入连接 A 和 B 的线段
    后,将 A 、 B 所在的树和并。如果 A 、 B 在同
    一棵树,那么就出现了封闭图形(因为 x 个
    点 x 条边的图必定出现圈)
    • 把坐标转换成编号的步骤,可以通过对坐
    标进行排序,再删除重复。
    • 时间 : O(MlogM) 例 5. 可爱的猴子
    • 树上挂着 n 只可爱的猴子(
    n ≤ 2*10 5 )。
    • 猴子 1 的尾巴挂在树上
    • 每只猴子有两只手,每只手可以抓住最多一只猴
    子的尾巴,也可以不抓。猴子想抓谁一定抓得到
    • 所有猴子都是悬空的,因此如果一旦脱离了树,
    猴子会立刻掉到地上。
    • 第 0 , 1 , ... , m ( 1 ≤ m ≤ 400 000 )秒中每一秒
    都有某个猴子把他的某只手松开,因此常有猴子
    掉在地上
    • 请计算出每个猴子掉到地上的时间 分析
    • 并查集?
    • " 时光倒流 "
    • 如何标记每只猴子的时间?
    -- 枚举并查集的元素
    -- 需要访问兄弟 / 儿子?
    -- 链表即可
    -- 不用指针 例 6. 奇数偶数
    • 你的朋友写下一个由 0 和 1 组成的字符串,并
    告诉你一些信息,即某个连续的子串中 1 的个
    数是奇数还是偶数。你的目标是找到尽量小
    的 i ,使得前 i+1 条不可能同时满足
    -- 例如,序列长度为 10 ,信息条数为 5
    -- 5 条信息分别为 1 2 even , 3 4 odd , 5 6 even , 1
    6 even , 7 10 odd
    • 正确答案是 3 ,因为存在序列 (0,0,1,0,1,1) 满
    足前 3 条信息,但是不存在满足前 4 条的序列 分析
    • 部分序列
    -- 可以从 s 的奇偶性恢复整个序列
    -- a b even 等价于 s[b], s[a-1] 同奇偶
    -- a b odd 等价于 s[b], s[a-1] 不同奇偶
    • 一开始每个 s[i] 自成一集合
    -- a b even Æ 合并 s[b], s[a-1]
    -- a b odd Æ ???
    • 矛盾的标志? 例 7. 团伙
    • 如果两个认识 , 那么他们要么是朋友要么是
    敌人 , 规则如下
    -- 朋友的朋友是朋友
    -- 敌人的敌人是敌人
    • 给出一些人的关系 ( 朋友、敌人 ), 判断一共
    最多可能有多少个团伙 例 8. 船
    • 给一个 01 矩阵 , 黑色代
    表船 . 右图有一个 29 吨
    的 , 3 个 7 吨的 , 两个 4 吨
    的和三个一吨的 .
    • 输入行数 N(<30000) 和
    每行的黑色格子 ( 区间
    数和每个区间 )
    • 输出每种重量的个数
    • 一共不超过 1000 个船 ,
    每个的重量不超过 1000 例 9. 离线最大值
    • 设计一个集合 , 初始为空,每次可以插入一
    个 1~n 的数 (1~n 各恰好被插入一次 ), 也可以
    删除最大值 , 要求 m 次操作的总时间尽量小 . 分析
    • 在最后加入 n-m 次虚拟的 MAX 操作 , 并记第 i
    个 MAX 操作为 M i , 记 M 1 之前的插入序列为 S 1,
    M i-1 (1<i<=m) 和 M i 之间的插入序列为 S i
    • 如果 n 在 S j 中被插入,则 M j 的输出一定是 n.
    然后删除 M j ,即 S j 合并到 S j+1 ,然后再
    查找 n-1 所在的序列 S k ,则 M k 的输出为 n-
    1... 如此下去,从 n 到 1 依次查找每个数所在
    序列,就可以得到它后面的 MAX 操作的结
    果 , 并把它和紧随其后的序列合并 例 10. 合并队列
    • 初始时 n 个数 1~n 各在单独的一列中 , 需要执
    行两个操作
    -- Move(i, j): 把 i 所在列接到 j 所在列的尾部
    -- Check(i, j): 询问 i 和 j 是否在同一列 , 如果是 , 输
    出二者之间的元素个数 分析
    • 每个数 i 的 p[i] 表示 i 和 p[i] 在同一队列 , 且 p[i]
    是 i 之前的第 d[i] 个元素
    • 对于队首 x, 有 p[x]=x, 附加变量 tot[x] 表示以
    x 为首的队列一共有多少个元素
    -- Mo v e 需要进行两次查找和一次合并
    -- Ch e c k 需要两次查找
    • FIN D: 修改 p[i] 时要修改 d[i]
    • ME R G E: 可以启发式合并么 ??? 四、哈希表 哈希表
    • 哈希表 (Hash table) 经常被用来做字典 (dictionary),
    或称符号表 (symbol-table) 直接存取表
    • 直接存取表 (Direct-access table) 的基本思
    想是 : 如果 key 的范围为 0~m-1 而且所有 key
    都不相同 , 那么可以设计一个数组 T[0..m-1],
    让 T[k] 存放 key 为 k 的元素 , 否则为空 (NIL)
    • 显然 , 所有操作都是 O(1) 的
    问题 : key 的范围可能很大 ! 64 位整数有
    18,446,744,073,709,551,616 种可能 , 而字
    符串的种类将会更多 ! 解决方案 : 哈希函数 哈希函数的选取
    • 严格均匀分布的哈希函数很难寻找 , 但一般
    说来有三种方法在实际使用中效果不错
    -- 取余法 : 取 h(k) = k mod m
    -- 乘积法 : 取 h(k) = (A*k mod 2 w ) rsh (w-r)
    -- 点积法 : 随机向量 a 和 k 的 m 进制向量做点积
    • 实践中经常采用取余法 取余法
    • 一般来说不要取 m=2 r , 因为它只取决于 k 的后 r 位
    • 一般取 m 为不太接近 2 或 10 的幂 乘积法
    • 取 m=2 r , 计算机字长为 w, 则
    H(k) = (A*k mod 2 w ) rsh (w-r)
    • 其中 rsh 表示右移位 , A 是满足 2 w-1 <A<2 w 的
    奇数 . 注意 : A 不应该太接近于 2 w . 由于乘法
    并对 2 w 取余是很快的 ( 自然就会丢弃高位 ),
    而算术右移也很快,因此函数计算开销小 冲突解决
    • 不同的 key 映射到同一个数 , 称为冲突
    (collision), 冲突的两个元素显然不可能放在
    哈希表的同一个位置
    • 通常有两种冲突解决方案 (resolving
    collisions)
    -- 链方法 (chaining): 把 key 相同的串成链表
    -- 开放地址法 (open addressing): 自己的位置被
    占了 , 就去占别人的 链地址法
    • Ke y 相同的元素形成一个链表 链地址法的查找效率
    • 链地址法的时间效率取决于 hash 函数的分布 . 我
    们假设每个 k 将等可能的被映射到任意一个 slot,
    不管其他 key 被映射到什么地方
    • 设 n 为 key 的数目 , m 为不同的 slot 数 , 装载因子α
    = n/m, 即每个 slot 平均的 key 数 , 则
    • n=O(m) 时期望搜索时间是 O(1) 的 开放地址法
    • 开发地址法只使用表内的空间 . 如果冲突产生 , 计
    算出第二个可能的位置 , 如果那里也有其他元素 ,
    再看第三个可能的位置 ... 即按一个 探测序列 查找
    • 位置应该是 key 和探测的次数 (probe number) 的函
    数 , 第 i 次探测位置为 slot = h(k,i)
    • 每个 slot 都应能被探测到 , 因此每个 k 的探测序列
    <h(k,0), h(k,1), h(k,2), ...,h(k,m-1)>
    • 都应是 {0,1,...,m-1} 的排列
    • 注意 : 开放地址法不容易 删除 元素 开放地址法示例 探测方法
    • 线性探测 :
    • 虽然很简单 , 但是容易形成元素堆积
    • 二次哈希 : 组合两个哈希函数
    • 一般效果会好很多,但应保证 h 2 (k) 和 m 互素 , 比如
    取 m 为 2 的幂而让 h 2 (k) 只产生奇数 开放地址法的查找效率
    • 假设每个 key 等概率的把 m! 个排列中的任何
    一个作为它的探测序列 , 则有
    定理 : 装载因子α <1 , 不成功查找的期望
    探测次数为 1/ (1- α)
    • 证明 : 第 i 次探测到非空 slot 的概率为
    (n-i)/(m-i) < n/m = α
    • 下面各种情况按概率加权计算期望 开放地址法的查找效率
    • 各种情况按概率加权 , 得期望的探测次数为 查找效率比较
    • 把开放地址法说通俗一点,就是
    -- 装载因子为常数时 , 期望查找次数为常数
    -- 一半装满时 , 期望查找次数为 1/(1-0.5)=2
    -- 装满 90% 时 , 期望查找次数为 1/(1-0.9)=10
    • 装得比较满 ( 尤其是 n>m) 时 , 使用链地址法
    -- 在哈希表里保存每个 key 的第一个元素
    first[0..m-1],
    -- 在一个数组 data[1..n] 里装着所有元素和下一个
    元素 next[1..n] 链地址法参考代码
    • 在实际应用中,推荐使用链地址法
    -- first[i] 表示 哈希函数值为 i 的第一个数据下标
    -- ke y [i] 和 next[i] 表示 i 数据的 key 和下一个
    int find(int k){
    int h = hash(k);
    int p = first[h];
    while (p){ if(key[p] == k) return p; p = next [p]; }
    return 0;
    }
    void insert(int x){
    int h = hash(key[x]); next[x] = first[h]; first[h] = x;
    }
相关推荐
lulu_gh_yu25 分钟前
数据结构之排序补充
c语言·开发语言·数据结构·c++·学习·算法·排序算法
ULTRA??1 小时前
C加加中的结构化绑定(解包,折叠展开)
开发语言·c++
凌云行者2 小时前
OpenGL入门005——使用Shader类管理着色器
c++·cmake·opengl
凌云行者2 小时前
OpenGL入门006——着色器在纹理混合中的应用
c++·cmake·opengl
~yY…s<#>2 小时前
【刷题17】最小栈、栈的压入弹出、逆波兰表达式
c语言·数据结构·c++·算法·leetcode
可均可可3 小时前
C++之OpenCV入门到提高004:Mat 对象的使用
c++·opencv·mat·imread·imwrite
白子寰3 小时前
【C++打怪之路Lv14】- “多态“篇
开发语言·c++
小芒果_013 小时前
P11229 [CSP-J 2024] 小木棍
c++·算法·信息学奥赛
gkdpjj3 小时前
C++优选算法十 哈希表
c++·算法·散列表
王俊山IT3 小时前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习