0%

LCA 问题

模板题

最近公共祖先简称 LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。 ——oi wiki

朴素算法

过程:

每次找深度比较大的那个点,让它向上跳。这两个点最后一定会相遇,相遇的位置就是想要求的 LCA。

单次查询的时间复杂度为 O(n),一般不被使用。(故省略代码

倍增算法

该算法是朴素算法的改进算法,也是最经典的 LCA 算法,本质上是增加朴素算法中跳跃的距离以减少操作次数。

时间复杂度:预处理 O(nlogn),单次查询 O(logn)。

思路:

  1. 预处理:

    • 利用 DFS 遍历树,记录每个节点的深度和每个节点的 2^k^级祖先。
    • 对于每个节点 u,记录它的 2^k^级祖先,k 从 0 到 $log_2N$.
  2. 查询:

    对于每次查询的两个节点 a 和 b

    • 将深度较大的节点向上跳到与另一个节点相同的深度。
    • 向上跳 2^k^步,直到找到 LCA.

    核心思想:

  • 利用二进制分解的思想,将跳跃步长分解为 2^k^ 的幂次。
  • 例如,如果需要跳 5 步,可以分解为 2^0^+2^2^(即跳 1 步 + 跳 4 步)。
  • 通过预处理每个节点的 2^k^ 级祖先,可以快速实现任意步长的跳跃。

具体步骤:

  1. 调整深度
    • 如果两节点深度不同,将深度较大的节点跳到另一个节点相同的深度
  2. 同时上跳
    • 两个节点深度相同时,同时上跳 2^k^ 步,知道找到 LCA。
    • 跳跃时,如果跳 2^k^ 步后两个节点的祖先不同,则跳;否则不跳。
    • 最终,两个节点会跳到 LCA 的直接子节点,此时它们的父节点就是 LCA。

代码:

相关变量

1
2
3
4
5
6
const int Log = 20;
const int maxn = 500010;

vector<int> tree[maxn]; //邻接表存树,需要建双向边
int depth[maxn]; //记录深度
int up[maxn][Log]; //up[u][k] 表示节点 u 的 2^k 级祖先

预处理部分

  • 计算深度:
    • depth[u] = depth[fa] + 1当前节点 u 的深度是其父节点 parent 的深度加 1。
    • 根节点深度为 0.
  • 2^k^级祖先:
    • up[u][0] = fa 当前节点 u 的 2^0^ 父节点是 fa
    • up[u][k] = up[up[u][k - 1]][k - 1]当前节点 u 的 2^k^ 级祖先可以通过其 2^k-1^级祖先的 2^k-1^ 级祖先得到。
  • 递归遍历子树:
    • 对于当前节点 u 的每一个子节点 v,如果 v 不是父节点 fa,则递归调用 dfs(v, u)
1
2
3
4
5
6
7
8
9
10
11
12
void dfs(int u, int fa){
depth[u] = depth[fa] + 1;
up[u][0] = fa;
for(int i=1 ; i<Log ; ++i){
up[u][i] = up[up[u][i-1]][i-1];
}
for(int v : tree[u]){
if(v!=fa){
dfs(v, u);
}
}
}

查询部分

  • 调整深度:

    • 如果 depth[a] < depth[b],交换 ab,确保 a 是深度较大的节点。
    • 计算深度差 diff = depth[a] - depth[b]
    • 通过 diff 的二进制表示,将 a 跳到与 b 相同的深度。
  • 检查是否找到:

    • 如果 ab 已经相等,说明 a(或 b)就是 LCA,直接返回。
  • 同步上跳:

    • 从最大的 k 开始(Log - 10),尝试将 ab 同时向上跳 2^k^ 步。
    • 如果 up[a][k] != up[b][k],说明跳 2^k^ 步后不会跳过 LCA,因此可以安全地跳。
    • 最终,ab 会跳到 LCA 的直接子节点,此时 up[a][0] 就是 LCA。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int lca(int a, int b){
if(depth[a]<depth[b]) swap(a,b);

int dif = depth[a] - depth[b];
for(int i=0 ; i<Log ; ++i){
if(dif & (1<<i)){
a = up[a][i];
}
}

if(a==b) return a;

for(int i=Log-1 ; i>=0 ; --i){
if(up[a][i] != up[b][i]){
a = up[a][i];
b = up[b][i];
}
}
return up[a][0];
}

主函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main()
{
int x, y;
int n, m, s;
cin >> n >> m >> s;

for(int i=1 ; i<n ; ++i){
cin >> x >> y;
tree[x].push_back(y);
tree[y].push_back(x);
}

dfs(s, 0);

for(int i=0 ; i<m ; ++i){
cin >> x >> y;
cout << lca(x,y) << '\n';
}

return 0;
}

数字 dfs

处理[l, r]区间内满足某种性质 P 的数

一般 l < r < 1e18,我们无法直接 for(l, r)

这时我们可以转移视角,不看这个数字,而是看数位,即看成 string,这时 1e18 也只有 19 位而已


华农

波奇酱确实不喜欢数学,所以他希望可以从一个区间中找出一些他喜欢的数。
如果一个数所有的相邻位数的差的绝对值为 1, 那么波奇酱就会喜欢它。特别地,所有一位数都是波奇酱喜欢的数。

给定区间 [L,R],请求出在这个区间内波奇酱喜欢的数的数量。

L,R(1≤L≤R≤1e18),表示给定的区间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ll  l, r;
int ans;
void dfs(ll num){
if(num > r)return;
if(num>=l && num<=r)ans++;
ll d = num%10;
if(d-1>=0)dfs(num*10+d-1);
if(d+1<=9)dfs(num*10+d+1);
return ;
}

void solve(){
cin >> l >> r;
for(int i = 0; i <= 9; i++){
dfs(i);
}
cout << ans << '\n';
return ;
}

天梯

本题就请你对任一给定的 n,求出给定区间内被 n 整除的 n 位数。

输入格式:
输入在一行中给出 3 个正整数:n(1<n≤15),以及闭区间端点 a 和 b(1≤a≤b<1e15)

输出格式:
按递增序输出区间 [a,b] 内被 n 整除的 n 位数,每个数字占一行。

若给定区间内没有解,则输出 No Solution

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
66
ll n, prel[N], prer[N];
vector<ll>ans;
string l, r;

ll qpow(ll a, ll b){
ll res = 1;
while(b){
if(b&1)res = a * res;
a = a * a;
b >>= 1;
}
return res;
}
string zh(ll sum){
string s;
while(sum){
s.push_back(sum%10+'0');//加'0'转字符串
sum/=10;
}
reverse(s.begin(), s.end());
return s;
}
void dfs(ll val, ll pos){
if(pos == n + 1){
ans.push_back(val);
return ;
}
ll sum = 0;
for(ll i = 0; i <= 9; i++){
sum = val * 10 + i;
if(sum >= prel[pos] && sum <= prer[pos]){
if(sum%pos==0)dfs(sum, pos+1);
}
}
return ;
}
void solve(){
cin >> n >> l >> r;
ll op = 10;
if(r.size() < n || l.size() > n){
cout << "No Solution" << '\n';
return ;
}
if(l.size() < n){
l = zh(qpow(10, n-1));
}
if(r.size() > n){
r = zh(qpow(10, n) - 1);
}
//特判处理
// 3 10 1000 -> 3 100 999
for(int i = 0; i < (int)l.size(); i++){
prel[i + 1] = (l[i]-'0') + prel[i] *op;
}
for(int i = 0; i < (int)r.size(); i++){
prer[i + 1] = (r[i]-'0') + prer[i] *op;
}
dfs(0, 1);
if(ans.size()==0){
cout << "No Solution" << '\n';
return ;
}
for(ll v : ans)cout << v << '\n';
return ;
}

蓝桥

求第 n 个奇偶交替的数

1<=n<=1e12

注意到这种求满足特定要求的数的问题,大部分都可以考虑数位 dfs

本题有点小不同,且听我慢慢道来

题目要我们求第 n 个数,显然从最低位去考虑是不行的,无法求出确实当前位后有多少数

所以我们考虑从最高位开始,一般数位 dfs 也都是从最高位开始

接下来我们手写几个数来看看

1
2
3
4
5
10  21 ... 101  121 ...
12 23 103 123
14 25 105 125
16 27 107 127
18 29 109 129

可以看出,对于 cnt 位的数 q,它的最高位可以取 1~9,其余的每一位可以取 0,2,4,6,8 或 1,3,5,7,9

即每一位有 5 种可能

接下来我们求第 n 个数有多少位

注意到 cnt 位的数有 9 * $5 ^ {cnt-1}$个数 n <= 1e12, cnt <= 17,所以我们累加即可

我们记 q 为要求的第 n 个数

1
2
3
4
5
6
7
8
9
10
11
12
ll sum = 0;
for(int i = 2; i <= 17; i++){
sum += 9 * qpow(5, i-1);
if(sum>=n){
cnt = i;
break;
}
}

if(cnt!=2)n -= (sum - 9 * qpow(5, cnt-1));
//这里n变为q在cnt位的数中是第几个

接着我们来确定最高位的数 op

同理,注意到 cnt 位,以 op 为最高位的数一共有$5 ^ {cnt-1}$个数,我们同样累加即可

1
2
3
4
5
6
7
8
9
10
11
12
sum = 0;
for(int i = 1; i <= 9; i++){
sum += qpow(5, cnt - 1);
if(sum >= n){
op = i;
break;
}
}
if(op!=1)n -= (op-1)*qpow(5, cnt-1);
//这里同样的,我们把n变为q在cnt位,以op为最高位的数中是第几个
cout << op;
//这里我们直接把q当成字符串输出

接下来求其余位上的数,由于它们的取值都是一样的,我们开始我们的 dfs

同理,注意到 cnt 位,以 op 为最高位,op’为次高位的数一共有$5 ^ {pos-1}$个数,我们同样累加即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void dfs(int pos){
if(!pos)return ;
ll sum = 0;
for(int i = 1; i <= 5; i++){
sum += qpow(5, pos-1);
if(sum>=n){
if(op&1)op = i * 2 - 2;
else op = i * 2 - 1;
//这里可以把op设为全局变量,也可以把dfs写成两个变量的
cout << op;
//这里同理直接把q当成字符串输出
if(i!=1)n -= (i-1)*qpow(5, pos-1);
//同样的,我们把n变为q在cnt位,以op为最高位,op'为次高位的数中是第几个
dfs(pos-1);
return ;
//注意这里一定要return,因为这里q是唯一确定的数,这也是与之前的数位dfs不同的地方
}
}
}
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
ll n, m, op, cnt;

ll qpow(ll a, ll b){
ll res = 1;
while(b){
if(b&1)res = res * a;
a = a * a;
b >>= 1;
}
return res;
}

void dfs(int pos){
if(!pos)return ;
ll sum = 0;
for(int i = 1; i <= 5; i++){
sum += qpow(5, pos-1);
if(sum>=n){
if(op&1)op = i * 2 - 2;
else op = i * 2 - 1;
cout << op;
if(i!=1)n -= (i-1)*qpow(5, pos-1);
dfs(pos-1);
return ;
}
}
}
void solve(){
cin >> n;
ll sum = 0;
for(int i = 2; i <= 17; i++){
sum += 9 * qpow(5, i-1);
if(sum>=n){
cnt = i;
break;
}
}
if(cnt!=2)n -= (sum - 9 * qpow(5, cnt-1));
sum = 0;
for(int i = 1; i <= 9; i++){
sum += qpow(5, cnt - 1);
if(sum >= n){
op = i;
break;
}
}
if(op!=1)n -= (op-1)*qpow(5, cnt-1);
cout << op;
dfs(cnt-1);
return ;
}

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment