本文参考《洛谷深入浅出进阶篇》,使用 OJ 为洛谷。有两个侧重点,第一个是侧重对例题多角度深度剖析,第二个是侧重对习题的思路讲解与代码实现。如果有机会录视频的话讲解视频会放在 Bilbil 上。博客请关注博客园和 优快云。
提醒,本文很多代码都使用
STL
容器,如果要看思路是没问题的,如果要学习代码的话需要有STL
基础,包括但不限于(vector
,set
,pair<int,int>
,multiset
,map
,priority_queue
)等。
例 1:P1102 A-B 数对
题目链接 A-B 数对
题目大意
给定 n n n 个数 a 1 ∼ a n a_1\sim a_n a1∼an 和一个正整数 C C C,请问有多少个数对 ( i , j ) (i,j) (i,j) 满足 a i − a j = C a_i-a_j=C ai−aj=C ?
思路剖析
从给定的方程式切入。即对于每一个 a j a_j aj 找到序列中有多少个数等于 a j + C a_j+C aj+C。
- 记录每种数出现的次数,存放在一个
box
的桶里面。
其中 box[i]
表示
i
i
i 这个数在序列中出现的次数。那么只需要遍历所有数的种类,例如 box[i]
需要找 box[i + C]
,则
i
i
i 的贡献就是 box[i]*box[i + c]
。
所需知识:map
的使用方法。
int n, C, t;
cin >> n >> C;
map <int, int> box;
while (cin >> t) box[t] ++;
int ans = 0;
for (auto i : box) ans += (box.count(i.first + C)? i.second * box[i.first + C] : 0);
cout << ans << endl;
- 通过排序使数具有单调性,再查找。
在一组具有单调性的数据中去查找某个数出现的次数有很经典的二分做法(书上的双指针做法反而落得下乘)。
所需知识:lower_bound
和 upper_bound
的使用方法。
int n, C, ans = 0;
cin >> n >> C;
map<int, int> mp;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i];
sort(a.begin() + 1, a.end());
for (auto i : span(a.begin() + 1, a.end())) {
ans += upper_bound(a.begin() + 1, a.end(), i + C) - lower_bound(a.begin() + 1, a.end(), i + C);
}
cout << ans << endl;
(这里想从索引
1
1
1 开始遍历,所以用了一个 C++20
才有的特性——视图,span
不拷贝数据而是直接访问这段区域的内存。 )
例 2:P1638 逛画展
题目链接 逛画展
题目大意
给定 n n n 个数 a 1 ∼ a n a_1\sim a_n a1∼an 且 0 < a i ≤ m 0<a_i\leq m 0<ai≤m。请选择一个最小的区间 [ l , r ] [l,r] [l,r] 使得 a l ∼ a r a_l\sim a_r al∼ar 包含 1 ∼ m 1 \sim m 1∼m 的所有数,若存在多个区间输入最左边的。
思路剖析
如果区间越大,就越有可能包括 m m m 种数,区间越小就越难包括 m m m 种数。
- 二分是一个很经典的处理单调性的方法。
这里的单调性指的是区间大小和包括 m m m 种数的难度成反比。所以可以二分区间大小,对于每个区间大小可以花费 O ( n ) O(n) O(n) 的时间遍历一遍是否存在这样的区间包括 m m m 种数,而区间大小的范围是 1 ∼ n 1\sim n 1∼n,所以时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn) 。
如何花费
O
(
n
)
O(n)
O(n) 的时间检查一个大小为 mid
的窗口是否能包括
m
m
m 种数?
设 category
为区间所包括的数的种类,cnt[i]
表示第
i
i
i 个数在区间内出现了多少次。
首先将前 mid
个数依次放进区间,如果这个数是第一次放进区间,即 cnt[a[i]] == 0
,那么我们把 category
加一。
开始移动时同理,我们需要把上一个数删掉,删完之后需要判断一下 cnt[a[i - 1]]
是否为
0
0
0,如果为
0
0
0,把 category
减一。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;
const int M = 2e3 + 7;
int n, m, a[N], cnt[M];
int check (int mid) {
int category = 0;
memset(cnt, 0, sizeof cnt);
for (int i = 1; i <= mid; i++) {
if (!cnt[a[i]]) category ++;
cnt[a[i]] ++;
}
if (category == m) return 1;
for (int i = 2; i <= n + 1 - mid; i++) {
cnt[a[i - 1]] --;
if (!cnt[a[i - 1]]) category --;
cnt[a[i + mid - 1]] ++;
if (cnt[a[i + mid - 1]] == 1) category ++;
if (category == m) return i;
}
return 0;
}
void slove () {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
int l = 1, r = n;
while (l < r) {
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
cout << check(l) << ' ' << check(l) + l - 1 << endl;
}
signed main () {
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
slove();
}
- 双指针也可以处理单调性的问题。
左指针 L L L 最初在起点 1 1 1 ,右指针 R R R 最初在第一个能使得 [ L , R ] [L,R] [L,R] 区间内包含 m m m 种数的位置。
如果左指针 L L L 向右移动会减少包含的数种类,那么就让右指针扩展,直到左指针往右移动不会减少数的种类。
否则 L L L 一直往右移动直到会减少包含数的种类。
当右指针到 n n n 的时候停止移动,此时若左指针不能移动就退出。
根据上面的规则,我们始终保持着 [ L , R ] [L,R] [L,R] 区间内包含 m m m 种数,所以我们只要在两个指针移动过程中记录区间最小值以及最小值的左端点。
不难发现, L L L 最多从 1 1 1 处移动到 n n n 处,即移动 n n n 次,所以是 O ( n ) O(n) O(n) 的时间复杂度。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7;
const int M = 2e3 + 7;
int n, m, a[N], cnt[M], category;
void slove () {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
int l = 1, r = 1;
for (int i = 1; i <= n; i++) {
if (!cnt[a[i]]) category ++;
cnt[a[i]] ++;
if (category == m) {
r = i;
break;
}
}
int L = l, R = r;
while (l <= n and r <= n) {
while (cnt[a[l]] > 1 and category == m) cnt[a[l++]] --;
if (r - l + 1 < R - L + 1) L = l, R = r;
if (cnt[a[l]] == 1 and r == n) break;
while (cnt[a[l]] == 1 and r < n) cnt[a[++r]] ++;
}
cout << L << ' ' << R << endl;
}
signed main () {
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
slove();
}
例 3:P1115 最大子段和
题目链接 最大子段和
题目大意
给定 n n n 个整数 a 1 ∼ a n a_1\sim a_n a1∼an,找到一对 ( l , r ) (l,r) (l,r) 使得 a [ l ] + a [ l + 1 ] + ⋯ + a [ r ] a[l]+a[l+1]+\cdots+a[r] a[l]+a[l+1]+⋯+a[r] 最大,输出最大值。
思路剖析
将题目问题化成表达式,针对表达式求解
- 利用前缀和,即需 s [ r ] − s [ l − 1 ] s[r]-s[l-1] s[r]−s[l−1] 最大
即对于每个前缀和 s [ i ] s[i] s[i] 我们只需要与 1 ∼ i − 1 1\sim i-1 1∼i−1 中最小的前缀和相减就是一种答案,然后再所有答案里取最大值即可。
(minn
初始取
0
0
0 即可,因为若所有数都是正数,显然全选上,ans
取无穷小,这里取
−
1
e
9
-1e9
−1e9)
#include <bits/stdc++.h>
using namespace std;
#define INF 1e9
const int N = 2e5 + 7;
int n, a[N];
void slove () {
cin >> n;
int minn = 0, ans = -INF;
for (int i = 1; i <= n; i++) {
cin >> a[i];
a[i] += a[i - 1];
ans = max(ans, a[i] - minn);
minn = min(minn, a[i]);
}
cout << ans << endl;
}
signed main () {
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
slove();
}
- 将整个问题拆分为若干小问题,一一求解出小问题后再合并成原问题——分治
要求 [ 1 , n ] [1,n] [1,n] 的最大子段和,设真正的最大子段和为 [ i , j ] [i,j] [i,j],则位置分布如下:
-
1 ≤ i ≤ j ≤ x 1\leq i\leq j\leq x 1≤i≤j≤x
-
1 ≤ i ≤ x ≤ j ≤ n 1\leq i \leq x \leq j \leq n 1≤i≤x≤j≤n
-
1 ≤ x ≤ i ≤ j ≤ n 1\leq x\leq i\leq j \leq n 1≤x≤i≤j≤n
即真正的区间位置分布有三种情况,在 x x x 的左边,包含 x x x,在 x x x 的右边。
对于第一、三种情况,我们可以将其作为一个全新的问题,这个问题又有三个位置分布的子问题,一直递归下去直到区间大小为 1 1 1 的时候直接返回。
对于第二种情况,可以 O ( n ) O(n) O(n) 暴力遍历求出从 x x x 开始一直往 l l l 走的连续最大区间,和一直往 r r r 走的连续最大区间,最后求和即可。
#include <bits/stdc++.h>
using namespace std;
#define INF 1e9
const int N = 2e5 + 7;
int n, a[N];
int getMax(int l, int r) {
if (l == r) return a[l];
int mid = l + r >> 1;
int sumL = 0, maxL = -INF, sumR = 0, maxR = -INF;
for (int i = mid; i >= l; i--) {
sumL += a[i];
maxL = max(maxL, sumL);
}
for (int i = mid + 1; i <= r; i++) {
sumR += a[i];
maxR = max(maxR, sumR);
}
return max(max(getMax(l, mid), getMax(mid + 1, r)), maxL + maxR);
}
void slove () {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
cout << getMax(1, n) << endl;
}
signed main () {
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
slove();
}
例 4:P7072 直播获奖
题目链接 直播获奖
题目大意
给出 n n n 个整数 a 1 ∼ a n a_1 \sim a_n a1∼an 和一个百分比 w % w\% w% 。要求计算对于每个 i i i, 1 ∼ i 1\sim i 1∼i 中第 m a x ( 1 , ⌊ i × w % ⌋ ) max(1,\lfloor i\times w\%\rfloor ) max(1,⌊i×w%⌋) 大的数是多少?
思路剖析
从 a a a 的数据范围入手。
- 数据范围小的时候,可以利用权值作为下标。
建立一个 box
数组,box[i]
表示分数为
i
i
i 的人数当前有多少人。
从
1
∼
n
1\sim n
1∼n 逐渐更新 box
值,更新完之后遍历一遍 box
数组,找到第
m
a
x
(
1
,
⌊
i
×
w
%
⌋
)
max(1,\lfloor i\times w\%\rfloor )
max(1,⌊i×w%⌋) 大的数是多少。
由于 a i a_i ai 不超过 600 600 600,故此题时间复杂度为 O ( 600 n ) O(600n) O(600n),勉强能通过。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
int a[N], box[600 + 7];
void slove () {
int n, w;
cin >> n >> w;
for (int i = 1; i <= n; i++) {
cin >> a[i];
box[a[i]] ++;
int sum = max(1, i*w/100);
for (int j = 600; j >= 0; j--) {
sum -= box[j];
if (sum <= 0) {
cout << j << ' ';
break;
}
}
}
}
signed main () {
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
slove();
}
但是如果 m m m 大了,很容易超时,所以还可以再优化。
- 发现一个性质:当 i i i 增加 1 的时候, m a x ( 1 , ⌊ i × w % ⌋ ) max(1,\lfloor i\times w\%\rfloor ) max(1,⌊i×w%⌋) 至多增加 1 1 1。
也就是说,第 i i i 次的结果只需要在第 i − 1 i-1 i−1 的状态中改变至多 1 1 1 次。所以考虑维护第 i − 1 i-1 i−1 次的状态,然后修改一次。
可以用两个 multiset
,第一个 multiset
维护最大的
m
a
x
(
1
,
⌊
i
×
w
%
⌋
)
max(1,\lfloor i\times w\%\rfloor)
max(1,⌊i×w%⌋) 个数,每次操作保证答案在第一个 multiset.begin()
处产生。
第二个 multiset
维护剩下的数。
当 i − 1 → i i-1\rarr i i−1→i,我们需要判断 m a x ( 1 , ⌊ i × w % ⌋ ) max(1,\lfloor i\times w\%\rfloor) max(1,⌊i×w%⌋) 是否增加:
- 若
m
a
x
(
1
,
⌊
i
×
w
%
⌋
)
max(1,\lfloor i\times w\%\rfloor)
max(1,⌊i×w%⌋) 不变,将新来的数
a
i
a_i
ai 插入第二个
multiset
中,然后拿第二个multiset.rbegin()
与第一个multiset.begin()
比较大小,更大的放进第一个multiset
,更小的放进第二个multiset
。 - 若
m
a
x
(
1
,
⌊
i
×
w
%
⌋
)
max(1,\lfloor i\times w\%\rfloor)
max(1,⌊i×w%⌋) 增加
1
1
1,将新来的数插入第二个
multiset
中,并把第二个multiset.rbegin()
插入第一个multiset
中。
因为每次只需要插入删除 1 ∼ 2 1\sim2 1∼2 次,总共是 n n n 次,所以时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn),是目前最优秀的解决方案。
(当然也可以用优先队列实现这个功能,详细可以搜索“对顶堆”)
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
int a[N];
void slove() {
int n, w;
cin >> n >> w;
multiset<int> big, less;
for (int i = 1; i <= n; i++) {
cin >> a[i];
int now = max(1, i * w / 100); // 当前需要维持的第 now 大
less.insert(a[i]);
// 确保 big 的大小为 now
if (big.size() < now) {
big.insert(*prev(less.end()));
less.erase(prev(less.end()));
} else {
// 调整 big 和 less 的平衡
if ((*prev(less.end())) > (*big.begin())) {
big.insert(*prev(less.end()));
less.erase(prev(less.end()));
less.insert(*big.begin());
big.erase(big.begin());
}
}
// 输出当前第 now 大
cout << (*big.begin()) << " ";
}
cout << endl;
}
int main() {
slove();
}
multiset
是多重集,对插入元素自动排序,但不去重。和 set
一样,prev(multiset.end())
是最大值的迭代器,multiset.begin()
是最小值的迭代器。但缺点是删除一个元素会把 multiset
内的相同元素全部删除。所以要想删除单独的元素,可以考虑使用迭代器删除,而要找到 multiset
中的最大值,可以使用 prev(multiset.end())
返回最后一个元素的迭代器。
例 5:P2671 求和
题目链接 求和
题目大意
给 n n n 个格子,每个格子 i i i 有颜色 c o l i col_i coli 和分数 n u m i num_i numi。
需要找到所有三元组 ( x , y , z ) (x,y,z) (x,y,z) 满足:
- x < y < z x < y < z x<y<z 且 y − x = z − y y - x = z - y y−x=z−y。
- c o l x = c o l z col_x=col_z colx=colz
而对于一个满足条件的三元组 ( x , y , z ) (x,y,z) (x,y,z) 其分数为 ( x + z ) ( n u m x + n u m z ) (x+z)(num_x+num_z) (x+z)(numx+numz),求所有合法的三元组的分数之和(对 10007 10007 10007 取模)。
思路剖析
题目给出等式,考虑对等式性质进行分析。
我们可以将所有相同颜色的格子编号存入一个数组中。
对于里面任意两项 i , j i,j i,j ,若它俩是一个合法三元组的 x , z x,z x,z 值,则需要找到一个 y y y 到 i i i 与 j j j 距离相等。
不难发现 x + z = 2 y x+z=2y x+z=2y , x , z x,z x,z 的奇偶性必须一致,否则无法找出这样的 y y y。
所以我们再把相同颜色的格子编号再分为奇数和偶数。
假如 1, 3, 5, 7
四个编号的颜色都相同,那么能贡献的分数显然可以用数学表达式写出来:
3
×
∑
i
×
n
u
m
i
+
∑
i
×
(
s
u
m
−
n
u
m
i
)
3\times \sum i\times num_i+\sum i\times(sum-num_i)
3×∑i×numi+∑i×(sum−numi)
设有
c
n
t
cnt
cnt 个数,对于一个三元组,其分数拆开算是 (
x
n
u
m
x
+
y
n
u
m
y
+
x
n
u
m
y
+
y
n
u
m
x
xnum_x+ynum_y + xnum_y+ynum_x
xnumx+ynumy+xnumy+ynumx) ,每个数都会有和剩下的
c
n
t
−
1
cnt -1
cnt−1 个数配对过一次,所以贡献里面一定有
(
c
n
t
−
1
)
∑
i
n
u
m
i
(cnt-1)\sum inum_i
(cnt−1)∑inumi,另一个式子则是余下的数整理得到,其中
s
u
m
sum
sum 表示这
c
n
t
cnt
cnt 个数的
n
u
m
num
num 之和。
设颜色都为
k
k
k 且格子编号都是奇的元素个数
c
n
t
cnt
cnt,那么其贡献的分数为:
(
c
n
t
−
1
)
×
∑
i
×
n
u
m
i
+
∑
i
×
(
s
u
m
−
n
u
m
i
)
(cnt -1)\times \sum i\times num_i+\sum i\times(sum-num_i)
(cnt−1)×∑i×numi+∑i×(sum−numi)
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5 + 7;
int num[N], col[N];
void slove () {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> num[i];
map<int, vector<int>> Ji, Ou;
for (int i = 1; i <= n; i++) {
cin >> col[i];
(i % 2) ? Ji[col[i]].push_back(i) : Ou[col[i]].push_back(i);
}
int ans = 0;
for (auto i : Ji) {
int sum = 0;
for (auto j : i.second) sum = (sum + num[j]);
for (auto j : i.second) {
ans = (ans + (i.second.size() - 1) * j * num[j] + j *(sum - num[j]) ) % 10007;
}
}
for (auto i : Ou) {
int sum = 0;
for (auto j : i.second) sum = (sum + num[j]);
for (auto j : i.second) {
ans = (ans + (i.second.size() - 1) * j * num[j] + j *(sum - num[j]) ) % 10007;
}
}
cout << ans << endl;
}
signed main () {
slove();
}
例 6:P4147 玉蟾宫
题目链接 玉蟾宫
题目大意
给出一个
N
×
M
N\times M
N×M 的矩形,里面的元素是 F
或 R
,请找出一个面积最大的只包含 F
的子矩形。
思路剖析
个人认为书上悬线法的拓展价值和思维价值不如单调栈做法,所以我们只讲单调栈做法。
转化为子问题:给定 n n n 个高度 h [ i ] h[i] h[i],找到一个区间 [ l , r ] [l,r] [l,r] 使得 m i n ( a l , a l + 1 , , ⋯ , a r ) × ( r − l + 1 ) 最大 min(a_l,a_{l+1},,\cdots, a_{r})\times (r-l+1) 最大 min(al,al+1,,⋯,ar)×(r−l+1)最大。
如何转换为这个问题的?一个常规的枚举:以第 i i i 行作为矩形的底,算 1 ∼ i 1 \sim i 1∼i 行这个矩形能找出的最大子矩形。
我们需要预处理出每一列的高度 h [ i ] h[i] h[i]。
如何解决红字的子问题?
对每个高度 h [ i ] h[i] h[i],求出能找到的最大宽度。
朴素的想法是遍历向左找,如果左边有一个
h
[
j
]
<
h
[
i
]
h[j]<h[i]
h[j]<h[i],那么不必再往左了,剩下的都是无效遍历。
对于 h [ i ] h[i] h[i] 来说,有效的只有 h [ i − 1 ] h[i-1] h[i−1], h [ i − 4 ] h[i-4] h[i−4] 以及左边更小的数。
可以发现 i i i 的左侧能被有效找到的序列是一个单调递增的序列。
设这个序列是 v v v,如果 h [ i ] > v . b a c k ( ) h[i]>v.back() h[i]>v.back(),那么将这个 h [ i ] h[i] h[i] 加到序列后面。
如果 h [ i ] ≤ v . b a c k ( ) h[i]\leq v.back() h[i]≤v.back(),那么以 h [ i ] h[i] h[i] 为高的矩形的最左边能走到的位置 是这个序列中第一个大于等于 h [ i ] h[i] h[i] 的数 所处的位置。
已经知道高度,还需要知道宽度,所以
v
v
v 序列中考虑维护一个 pair<int,int>
对,first
是高度,second
是以 first
为高的矩形的下标。
则以 h [ i ] h[i] h[i] 高的矩形的宽度是 v v v 序列中第一个大于等于 h [ i ] h[i] h[i] 的数 所处的位置。
由于有一个更小的 h [ i ] h[i] h[i] 出现,所以这些大于等于 h [ i ] h[i] h[i] 的数是无效的了,直接删除即可。
但是我们只算了以 h [ i ] h[i] h[i] 为高的矩形的左边,如何算右边?
由于每个 h [ i ] h[i] h[i] 只会被 i + 1 ∼ m i+1\sim m i+1∼m 中第一个小于等于它的值更新掉,设这个值为 h [ j ] h[j] h[j],所以 h [ i ] h[i] h[i] 的右边能扩展的宽度是 i ∼ j − 1 i\sim j-1 i∼j−1 。
所以每次我们从序列的
b
a
c
k
back
back 删除,然后删除的过程中累计一下宽度 temp
,并实时用
v
.
f
i
r
s
t
×
t
e
m
p
v.first\times temp
v.first×temp 更新答案。
每个数会被加入和删除一次(查找总时间与删除所需时间一样),每一行的时间复杂度是 O ( m ) O(m) O(m),总时间复杂度是 O ( n m ) O(nm) O(nm)。
#include <bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
const int N = 1e3 + 7;
int h[N][N]; // (i, j) 为 'F' 的时候的往上走能走多高
char ch[N][N];
void slove () {
int n, m, ans = 0, temp = 0;
cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
cin >> ch[i][j];
for (int i = 1; i <= n; i++) {
vector<PII> v;
for (int j = 1; j <= m; j++) {
if (ch[i][j] == 'F') h[i][j] = h[i - 1][j] + 1;
temp = 0;
while (!v.empty() and v.back().first >= h[i][j]) {
temp += v.back().second;
ans = max(ans, temp*v.back().first);
v.pop_back();
}
temp ++; // 加上 h[i][j] 本身的宽度 1
v.emplace_back(h[i][j], temp);
ans = max(ans, temp * h[i][j]);
}
temp = 0;
while (!v.empty()) {
temp += v.back().second;
ans = max(ans, temp*v.back().first);
v.pop_back();
}
}
cout << 3*ans << endl;
}
signed main () {
slove();
}
例 7: P2866 糟糕的一天
题目链接 糟糕的一天
题目大意
给定 n n n 个数 a 1 ∼ a n a_1\sim a_n a1∼an,对每个 a [ i ] a[i] a[i] 都求出一个最大的 j j j ,使得 a [ i + 1 ] ∼ a [ j ] a[i+1]\sim a[j] a[i+1]∼a[j] 都小于 a [ i ] a[i] a[i],设 C i = j − i C_i=j-i Ci=j−i,并对 C i C_i Ci 求和 。
思路剖析
对每个 i i i 都找到其右边第一个大于 a [ i ] a[i] a[i] 的数的位置。
朴素做法显然是暴力,但是要想通过这道题还需要对其进行仔细的观察。
如果出现了一个较大的值 a [ j ] a[j] a[j], a [ j ] a[j] a[j] 大于 a [ 1 ] ∼ a [ j − 1 ] a[1]\sim a[j-1] a[1]∼a[j−1] 中的所有数,那么 C 1 ∼ j − 1 C_{1\sim j-1} C1∼j−1 一定能全部求完。
这启发我们,维护一个序列 v v v,不断地按顺序往序列添加 a a a 中的元素。
如果当前的元素 a [ i ] a[i] a[i] 大于序列的最后一个数的时候,设这个数是 a [ j ] a[j] a[j],那么显然 C j = i − j C_j=i-j Cj=i−j 了。
然后把序列 v v v 后面的数一直删除,直到 a [ i ] a[i] a[i] 小于序列 v v v 的最后一个数。
如果当元素 a [ i ] a[i] a[i] 小于序列的最后一个元素,那么就加到序列末尾。
所以序列最好维护一个 pair<int,int>
,first
是高度,second
是下标。
每个数会被删除最多一次,而查找到答案的总时间等于删除的总时间,即 O ( n ) O(n) O(n)。
#include <bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define int long long
const int N = 8e4 + 7;
int n, ans;
int a[N];
void slove () {
cin >> n;
vector<PII> v; // first 是高度,second 是位置
for (int i = 1; i <= n; i++) {
cin >> a[i];
while (!v.empty() and v.back().first <= a[i]) {
ans += i - v.back().second - 1;
v.pop_back();
}
v.emplace_back(a[i], i);
}
while (!v.empty()) {
ans += n - v.back().second;
v.pop_back();
}
cout << ans << endl;
}
signed main () {
slove();
}
例 8:P1950 长方形
题目链接 长方形
题目大意
给定一个 n × m n\times m n×m 的网格图,每个格子 ( i , j ) (i,j) (i,j) 要么是障碍物要么是空地 。我们需要找出一个不包含障碍物的矩形,请问这样的矩形有多少个?
- 矩形的规格至少为 1 × 1 1\times 1 1×1。
- 矩形的四个顶点必须在网格上。
- 矩形的任意一个顶点坐标不同就认为该矩形不同。
思路剖析
枚举第 i i i 行作为我们找到的矩形的底部所在的行,求出。
设 S ( i ) S(i) S(i) 表示网格中以第 i i i 行作为底部的矩形个数。
答案是
∑
i
=
1
n
S
(
i
)
\sum_{i=1}^{n}S(i)
i=1∑nS(i)
对于第
i
i
i 行的第
1
∼
m
1\sim m
1∼m 列都记录一个高度
h
[
j
]
h[j]
h[j],表示
(
i
,
j
)
(i,j)
(i,j) 这个格子及其上方有多少个连续的空地。
怎么去枚举才能使得统计的矩形不重不漏?
对于每一列 j j j,求出左侧有多少个连续的 h h h 值大于等于 h [ j ] h[j] h[j],设为 L [ j ] L[j] L[j],右侧有多少个连续的 h h h 值大于 h [ j ] h[j] h[j],设为 R [ j ] R[j] R[j] ,则包含第 j j j 列的矩形个数为: ( L [ j ] + 1 ) × ( R [ j ] + 1 ) × h [ j ] (L[j]+1)\times(R[j]+1)\times h[j] (L[j]+1)×(R[j]+1)×h[j]
记住是左闭右开的形式,这需要一定的枚举功底,毕竟是省选题(如果觉得不理解,建议提升实力后再回来写这题,作者在有区域赛铜牌的实力之后才开始写蓝题)。
现在问题就变成了对于一个 j j j,求左边有多少个连续的数大于等于 h [ j ] h[j] h[j],右边有多少个连续的数大于 h [ j ] h[j] h[j]。
这是经典的单调栈性质。
因为不难发现,如果往左走遇到了一个小于 h [ j ] h[j] h[j] 的数,那么就不需要再往左走了。
此时设往左走最后一个大于等于 h [ j ] h[j] h[j] 的数的下标是 k k k,我们还需要维护一个 L L L 数组,表示每个矩形能往左扩展的最长距离,此时 L [ j ] = L [ k ] L[j]=L[k] L[j]=L[k]。
维护了这个单调栈后,怎么求出右边有多少个连续的 h h h 值大于 h [ j ] h[j] h[j] 呢?
显然 h [ j ] h[j] h[j] 会在有一个小于等于 h [ j ] h[j] h[j] 的值的数出现的时候被删除,所以我们可以在它被删除的时候计算右边能扩展的距离。
#include <bits/stdc++.h>
using namespace std;
//#pragma GCC optimize(2)
#define int long long
#define endl '\n'
#define PII pair<int,int>
#define INF 1e18
void slove () {
int n, m, ans = 0; cin >> n >> m;
vector<vector<char>> ch (n + 1, vector<char>(m + 1));
vector<vector<int>> h (n + 1, vector<int>(m + 1, 0));
for (int i = 1; i <= n; i++) {
int sum = 0;
vector <PII> v;
vector <int> L(m + 1, 0);
for (int j = 1; j <= m; j++) {
cin >> ch[i][j];
h[i][j] = ((ch[i][j] == '*') ? 0 : h[i - 1][j] + 1);
while (!v.empty() and h[i][j] <= v.back().first) {
L[j] = L[v.back().second];
int len = (v.back().second - L[v.back().second] + 1) * (j - v.back().second);
sum += len * v.back().first;
v.pop_back();
}
v.emplace_back(h[i][j], j);
if (!L[j]) L[j] = j;
}
while (!v.empty()) {
int len = (v.back().second - L[v.back().second] + 1)*(m - v.back().second + 1);
sum += len * v.back().first;
v.pop_back();
}
ans += sum;
}
cout << ans << endl;
}
signed main () {
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
slove();
}
例 9:P2032 扫描
题目链接:扫描
题目简述
给你 n n n 个数 a 1 ∼ a n a_1\sim a_n a1∼an 和一个能盖住连续 k k k 个数的木板。一开始木板盖住了第 1 ∼ k 1\sim k 1∼k 个数,每次将木板向右移动一个单位,直到右端与第 n n n 个数重合。每次移动前输出被覆盖住的数字中最大的数是多少?
思路剖析
数据结构维护静态区间最大值
这是一个很无脑的做法,也是我最不推荐用这种做法去做这道题的,因为根本没有思考只是单纯的套模板就写出来了。
单调队列
(算法并不是一猜就猜出来的,需要我们观察到足够多的性质之后才知道要用什么算法)
假设 K = 4 K = 4 K=4,在这张图内木板覆盖的最大数是 9 9 9,所以 1 , 3 1,3 1,3 是没用的数。
观察到直接把最大值之前的数删去是不会影响到答案的。
但其实还不是很全面,因为进来的值有可能不是当前的最大值,但是可能在未来会成为最大值。
对于每个进来的数把它前面的小于它数删除是没有影响的。
那么这题就做完了,只需要维护一个序列,然后按照红色字体的性质进行操作即可。
#include <bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
signed main () {
deque<PII> q;
int n, k; cin >> n >> k;
for (int i = 1; i <= n; i++) {
int t; cin >> t;
while (!q.empty() and q.back().first <= t) q.pop_back();
q.push_back(PII{t, i});
if (i >= k) cout << q.front().first << endl;
if (q.front().second <= i - k + 1) q.pop_front();
}
}