启发式分裂
设想以下情形:我们进行一种分治算法。每次我们寻找一个点,将区间分成两半进行递归。
对于每一层,分割点的判定是独立的,不需要依赖任何其它点。而且每一层我们不需要遍历所有的点。这种性质允许我们用一种方式,将最坏可能卡到 \(\mathcal{O}(n ^ 2)\) 的分治,优化成 \(\mathcal{O}(n \log n)\)。
我们都知道启发式合并:考虑将 \(n\) 个元素合并成一个大集合的过程,每次给定两个集合要求合并,我们可以将 \(a\) 的元素加入 \(b\),或者将 \(b\) 的元素加入 \(a\),因此,对时间的贡献是 \(|a|\),\(|b|\) 其中一个。
假如每次,我们将小的集合向大的集合合并,那么我们考虑一个初始元素 \(t\),\(t\) 所在的集合每次在合并中贡献复杂度时,\(t\) 所在的集合的大小至少翻倍。因此,\(t\) 所在的集合最多会在 \(\log n\) 次合并中贡献复杂度。因此,\(t\) 最多会被处理 \(\log n\) 次,总复杂度为 \(\mathcal{O}(n \log n)\)。
考虑开头提出的分治算法:朴素的想法是从 \(l\) 开始遍历,找到分割点 \(p\),后面的就不用遍历了。因此,我们可以把在一个区间 \(seg\) 上寻找一个分割点的过程,看作将两个小区间合并成 \(seg\)。
如果每次 \(p - l + 1\) 都不到 \(r - l + 1\) 的一半。那么就相当于每次将短的区间合并到长区间里,复杂度是 \(\mathcal{O}(n \log n)\) 的。我们想保证每个递归区间内贡献的遍历次数均不超过区间长度的一半,那么我们可以从区间两端同时开始,往中间遍历就好了。
P16902 [CCO 2026] Melborp
题目描述
Seta 正在为 CCO 出题!她想到了如下问题:
给定一个值域在 \([1, N]\) 的数组 \(A[1,\ldots,N]\),定义 \(B[i]\) 为满足 \(\ell \le i \le r\) 且 \(A[i] = \min(A[\ell], A[\ell + 1], \ldots, A[r - 1], A[r])\) 的二元组 \((\ell, r)\) 的个数。
输出数组 \(B[1,\ldots,N]\)。
然而,就在 CCO 的前一天,Seta 的电脑崩溃了,她只恢复了输出文件。给定输出数组 \(B[1,\ldots,N]\),你能否编写一个程序,重新构造出输入数组 \(A[1,\ldots,N]\) 呢?
Seta 提醒你,数组 \(A\) 并不唯一,任何合法的数组都会被她接受。
输入格式
输入的第一行包含一个整数 \(N\)。第二行包含 \(N\) 个以空格分隔的整数 \(B[1],\ldots,B[N]\) (\(1 \le B[i] \le N^2\))。
输出格式
输出 \(N\) 个以空格分隔的整数,即数组 \(A[1],\ldots,A[N]\),其中 \(1 \le A[i] \le N\)。数据保证至少存在一个合法的数组 \(A\)。
如果存在多个合法数组,你可以输出其中任意一个。特别地,即使原始的数组 \(A\) 是一个排列,你的答案也不必是排列。
输入输出样例 #1
输入 #1
3
3 1 2
输出 #1
1 3 2
输入输出样例 #2
输入 #2
2
2 2
输出 #2
1 1
输入输出样例 #3
输入 #3
3
1 4 1
输出 #3
2 1 3
说明/提示
样例输入 1 的输出解释
- 子数组 \([1,3,2]\)、\([1,3]\)、\([1]\) 的最小值均为 \(1\)。满足条件的子数组有 \(3\) 个。
- 子数组 \([3]\) 的最小值为 \(3\)。满足条件的子数组有 \(1\) 个。
- 子数组 \([3,2]\) 和 \([2]\) 的最小值均为 \(2\)。满足条件的子数组有 \(2\) 个。
样例输入 3 的输出解释
注意,\(A = [2,1,2]\) 同样会被评测机接受。
下面的表格展示了可获得的 \(25\) 分的分配情况:
| 分值 | \(N\) 的范围 | 额外限制 |
|---|---|---|
| \(2\) 分 | \(1 \le N \le 8\) | 无。 |
| \(3\) 分 | \(1 \le N \le 5\,000\) | 原始数组 \(A\) 是一个排列。 |
| \(5\) 分 | \(1 \le N \le 3 \times 10^5\) | ^ |
| \(5\) 分 | ^ | 无。 |
| \(5\) 分 | \(1 \le N \le 5 \times 10^6\) | 原始数组 \(A\) 是一个排列。 |
| \(5\) 分 | ^ | 无。 |
思路
如果你做过交互,你就会发现,如果在区间 \([l,r]\) 中,\(b_k = (k - l + 1)(r - k + 1)\),那么 \(a_k\) 一定是 \([l,r]\) 上的最小值。因为最小值两侧的问题是独立的,我们可以分别递归 \([l,k-1]\) 和 \([k+1,r]\) 还原序列。
处理一下 \([l,r]\) 中有多个最小值的情况:我们在递归的时候同时传入一个参数 \(cnt\),代表这个区间内的最小值填多少。如果我们遇到 \(b_k > (k - l + 1)(r - k + 1)\) 的情况,那么 \(a_k = cnt - 1\),往下递归时 \(cnt\) 不变。如果遇到了预期中的 \(b_k > (k - l + 1)(r - k + 1)\),那么 \(a_k = cnt\),往下递归时 \(cnt \leftarrow cnt + 1\) 即可。这里注意,同一个区间内,两种情况不可能同时出现!
上述算法可以使用我们前面提到的小技巧优化。
// Problem: P16902 [CCO 2026] Melborp
// Contest: Luogu
// Memory Limit: 512 MB
// Time Limit: 3000 ms
#include <bits/stdc++.h>
#define int long long
#define Misaka namespace
#define Network std
using Misaka Network;
const int N = 5e6 + 7;
int n, b[N], a[N];
void solve(int l, int r, int cnt){
if(l > r) return;
int L = l, R = r;
while(L <= R){
int x = (L - l + 1) * (r - L + 1);
if(b[L] == x){
a[L] = cnt;
solve(l, L - 1, cnt + 1), solve(L + 1, r, cnt + 1);
return;
}
else if(b[L] > x){
a[L] = cnt - 1;
solve(l, L - 1, cnt), solve(L + 1, r, cnt);
return;
}
x = (R - l + 1) * (r - R + 1);
if(b[R] == x){
a[R] = cnt;
solve(l, R - 1, cnt + 1), solve(R + 1, r, cnt + 1);
return;
}
if(b[R] > x){
a[R] = cnt - 1;
solve(l, R - 1, cnt), solve(R + 1, r, cnt);
return;
}
L ++, R --;
}
}
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for(int i = 1; i <= n; i ++) cin >> b[i];
solve(1, n, 1);
for(int i = 1; i <= n; i ++) cout << a[i] << ' ';
return 0;
}

浙公网安备 33010602011771号