本文实际上是我于2022/7/29补写的
分治
逆序对(洛谷P1908)
我们直接打暴力,枚举每一个树a[i],在a[1~n]直接寻找比a[i]小的数即可。
但是这样很明显不能通过本题,他的时间复杂度是 O ( n 2 ) O(n^{2}) O(n2)
我们可以用线段树维护权值数组来加速,但是今天的重点是分治。
我们可以观察到,对于逆序对*(x,y)*,只有3种情况:
- x,y在左边
- x,y在右边
- x左边y右边
所以我们能实现一个函数 calc(l, r):
- 调用 calc(l, mid) 获得左边块内的逆序对个数
- 调用 calc(mid, r) 获得右边块内的逆袭对个数
- 亲自去统计跨块的逆序对个数
即为在归并的过程中统计个数。
#include <bits/stdc++.h>
using namespace std;
int a[500005], n;
void input() {
scanf("%d", &n);
for(int i=0; i<n; i++)
scanf("%d", &a[i]);
}
long long ans;
// 统计 a[l, r) 区间内的逆序对的个数
void calc(int l, int r) {
if(l == r - 1)
return;
int mid = (l + r) / 2;
calc(l, mid); // 统计左边块内的逆序对个数
calc(mid, r); // 统计右边块内的逆序对个数
// 亲自统计跨块逆序对个数:x在左边,y在右边
for(int x=l; x<mid; x++) // 枚举左边块的 x
for(int y=mid; y<r; y++) // 枚举右边块的 y
if(a[x] > a[y]) ans++; // 发现一个跨块逆序对
}
void work() {
calc(0, n);
printf("%lld\n", ans);
}
int main() {
input();
work();
return 0;
}
但是我们用Master定理一算,嗯?还是 O ( n 2 ) O(n^{2}) O(n2)啊!
实现一个函数 calc(l, r):
- 调用 calc(l, mid)
- 调用 calc(mid, r)
- 将左边、右边的块排序
- O(n) 亲自统计跨块的逆序对个数
#include <bits/stdc++.h>
using namespace std;
int a[500005], n;
void input() {
scanf("%d", &n);
for(int i=0; i<n; i++)
scanf("%d", &a[i]);
}
long long ans;
int tmp[500005];
// 统计 a[l, r) 区间内的逆序对的个数
// 顺便给 a[l, r) 排序
void calc(int l, int r) {
if(l == r - 1)
return;
int mid = (l + r) / 2;
calc(l, mid); // 统计左边块内的逆序对个数
calc(mid, r); // 统计右边块内的逆序对个数
// 现在 a[l, mid) 和 a[mid, r) 都是有序的
// 亲自统计跨块逆序对个数:x在左边,y在右边
int y = mid;
for(int x=l; x<mid; x++) { // 枚举左边的 x
while(y < r && a[y] < a[x]) y++;
ans += y - mid; // a[mid, y) 全都小于 a[x],所以统计进答案
}
// 以下是合并左右两个有序数组,保证 a[l, r) 在调用结束后有序
// 归并排序
int p=l, q=mid, cur=l;
while(p < mid && q < r){
if(a[p] < a[q])
tmp[cur++] = a[p++];
else
tmp[cur++] = a[q++];
}
while(p < mid)
tmp[cur++] = a[p++];
while(q < r)
tmp[cur++] = a[q++];
for(int i=l; i<r; i++)
a[i] = tmp[i];
}
void work() {
calc(0, n);
printf("%lld\n", ans);
}
int main() {
input();
work();
return 0;
}
省了2个sort,就降下来了时间复杂度:
T
(
n
)
=
2
T
(
n
/
2
)
+
O
(
n
)
=
O
(
n
l
o
g
n
)
T(n) = 2T(n/2) +O(n) = O(nlog n)
T(n)=2T(n/2)+O(n)=O(nlogn)
快速幂
因为取模可以在运算过程中取,所以我们可以不爆空间。
计算
x
a
m
o
d
p
x_{a} mod p
xamodp 时,分两种情况:
若
2
∣
a
,则答案为
(
x
a
/
2
)
2
m
o
d
p
若
2
∤
a
,则答案为
(
x
⌊
a
/
2
⌋
)
2
⋅
x
m
o
d
p
若 2 | a,则答案为 (x^{a/2})2 mod p\\ 若 2 ∤ a,则答案为 (x^{⌊a/2⌋})2 · x modp
若2∣a,则答案为(xa/2)2modp若2∤a,则答案为(x⌊a/2⌋)2⋅xmodp
ll quickPow(ll a, ll x, ll p) {
if(x == 0) return 1;
if(x == 1) return a % p;
ll tmp = quickPow(a, x/2, p);
if(x % 2 == 0)
return tmp * tmp % p;
else
return tmp * tmp % p * a % p;
}
二分
平方根
题目描述
求 n \sqrt n n 的整数部分。输入格式
仅一行,一个正整数,表示 n n n。
输出格式
仅一行,一个正整数,表示$\lfloor \sqrt n\rfloor $。
样例 #1
样例输入 #1
12345
样例输出 #1
111
提示
对于 100 % 100\% 100%的数据, n ⌊ 1 0 18 ⌋ n\lfloor 10^{18}\rfloor n⌊1018⌋。
可以转换为求最大的x使 x 2 ≤ n x^{2}\leq n x2≤n
暴力
可以直接暴力
for(ll i=1;i*i<=n;i++);
printf("%lld",x-1);
但是x达到了 1 18 1^{18} 118级别,无法通过本题。
二分
我们可以通过猜来解决问题。猜一个数x如果 x 2 x^{2} x2大于n就说明我们猜大了,如果小于就说明猜小了。
在1~1e9猜数即可:
void work() {
ll l = 0, r = 1000000000;
// 在 [l, r] 范围内寻找答案
// l, r 差距大于 5 的时候,执行二分
while(r - l > 5) {
ll mid = (l + r) / 2;
// 猜中点,判断答案与之的大小关系
if(mid * mid > n) // mid 猜大了,答案比 mid 小
r = mid - 1;
else // mid^2 <= n,答案 >= mid
l = mid;
}
// l, r 差距 <= 5,逐个判断
// 寻找最大的 x*x <= n 的 x
for(ll x=r; x>=l; x--)
if(x * x <= n) {
cout << x << endl;
return;
}
}
可能会有人问:“嗯?你这个二分怎么和一般的二分不一样?”
因为这样好写,不容易写炸。如果写“学院派”二分可能不小心就写炸了。
这样写之比“学院派"二分慢一点点,称为”通用二分“。
我们缩小到5个后逐个判断。
可二分性
如果你可以快速判断一个答案是否满足要求,那就满足可二分性,可以尝试二分求解。
区间和(前缀和)
今有 n 个同学,学号为 1 ∼ n。给出每个人的语文成绩。 需要处理 m 次询问,每次询问给定 l, r,查询学号在 [l, r] 范围内的学生的成绩
sum。
即给定一个数组,多次查询区间和。
如果采用分块,每次查询代价是 O ( n ) O(\sqrt n) O(n),总复杂度 O ( m n ) O(m\sqrt n) O(mn)
那么我们可以弄一个pre数组,来储存1k*的和,求*ik的和即为pre[k]-pre[i] 真是方便呢!
这个pre数组便被成为前缀和。
所以前缀和实际上也不是个很高级的东西。
void init() {
// pre[k] = a[1] + a[2] + ... + a[k]
for(int i=1; i<=n; i++)
pre[i] = pre[i-1] + a[i];
}
// a[l] + a[l+1] + ... + a[r]
int getSum(int l, int r) {
return pre[r] - pre[l-1];
}
仅适用于数组固定,如果是动态(插入+查询)的那种就不能使用。
我们需要使用 O(n) 的时间预处理出 pre 数组。 接下来每个询问可以 O(1) 回答。
复杂度在 OI 中简记为 O(n) −O(1),即 O(n) 预处理,每次查询 O(1)。
差分
{%label 差分 red %}可以用来解决{%label 多次区间加,最终求整个数组 red %}的问题。 我们采用 d[x] 记录{%label a 数组从 x 这个位置开始应该增加多少 red %}。 那么,[l, r] 区间加 k 可以表示为:
- d[l] += k
- d[r+1] -= k
最后扫一遍 d 数组即可还原 a[]。
a={0,0,+1,0,0,0,-1,0,0};
pre={0,0,1,1,1,1,0,0};
这样即可实现区间加/减。
不用直接给数组加减,我们只需维护差分数组即可。
前缀和和查询何为逆运算。
想要给1~3加1,只需要给差分数组的[1]+1,[3]-1即可。
在所有的都做完差分后做一次差分即可
O
(
n
)
O(n)
O(n)完成运算。
浇花
题目描述
今有 n n n 盆花摆成一排,编号为 1 ∼ n 1\sim n 1∼n。
机器猫一时兴起,就会去浇花。机器猫每次浇花,是选择一个区间 [ l , r ] [l, r] [l,r],给这个区间内的花浇水。
机器猫一共浇了 m m m 次水。我们想知道,每盆花被浇了多少次。
输入格式
第一行,两个正整数 n , m n, m n,m。
接下来 m m m 行,每行两个正整数 l , r l, r l,r,表示一次浇花。
输出格式
仅一行, n n n 个整数,表示每盆花被浇了多少次。
样例 #1
样例输入 #1
5 3 1 4 2 5 3 3
样例输出 #1
1 2 3 2 1
提示
对于 100 % 100\% 100% 的数据, n , m ≤ 100000 n, m\leq 100000 n,m≤100000。
void add(int l, int r) {
d[l] += 1;
d[r+1] += -1;
}
void work() {
while(m--) {
int l, r;
cin >> l >> r;
add(l, r);
}
// 通过 d 数组恢复 a 数组
for(int i=1; i<=n; i++)
a[i] = a[i-1] + d[i];
for(int i=1; i<=n; i++)
cout << a[i] << ' ';
}
差分是不是非常的有意思和强大呢?对于固定的修改可以很方便的操作。
差分与前缀和
差分可以应对多次输入区间加,最后输出整个序列的问题。
差分的缺陷是不能中途询问,因为中途查询的复杂的是*O(n)*的。
差分与前缀和是两个极端,一个中途不能改,一个中途不能查。
- 差分可以*O(1)*改,*O(n)*查询。
- 前缀和可以*O(n)*预处理,*O(1)*查询
(线段树可以中途改和查且复杂的为O(nlogn)
Two-pointers
e g 1 eg_1 eg1:
最短区间
题目描述
给定长度为 n n n 的序列 a a a ,以及一个正整数 s s s。
要寻找最短的区间,使区间 sum ≥ s \geq s ≥s。
输出最短区间的长度。
输入格式
第一行,两个正整数 n , s n, s n,s。
接下来一行, n n n 个正整数,表示序列 a a a。
输出格式
仅一行,一个正整数,表最短合法区间的长度。
样例 #1
样例输入 #1
7 11 1 3 2 5 3 4 1
样例输出 #1
3
样例 #2
样例输入 #2
7 5 1 3 2 5 3 4 1
样例输出 #2
1
提示
对于 40 % 40\% 40% 的数据, n ≤ 1000 n \leq 1000 n≤1000。
对于 100 % 100\% 100% 的数据, n ≤ 100000 , 1 ≤ a i ≤ 10000 n\leq 100000, 1\leq a_i \leq 10000 n≤100000,1≤ai≤10000。
我们可以想到直接
O
(
n
2
)
O(n^{2})
O(n2)暴力,但是文明人都不会暴力。所以我们想到了二分,可以
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)解决。枚举l二分r。
#include <bits/stdc++.h>
using namespace std;
int n, s;
int a[100005];
void input() {
cin >> n >> s;
for(int i=1; i<=n; i++)
cin >> a[i];
}
int pre[100005];
void init() {
for(int i=1; i<=n; i++)
pre[i] = pre[i-1] + a[i];
}
int getSum(int l, int r) {
return pre[r] - pre[l-1];
}
// 寻找到一个位置 pos,使得 [begin, pos] 区间和 >=s,且 pos 最小
int findRight(int begin) {
int l = begin, r = n;
while(r - l > 5) {
int mid = (l + r) / 2;
if(getSum(begin, mid) <= s)
l = mid;
else
r = mid;
}
for(int x=l; x<=r; x++)
if(getSum(begin, x) >= s) // [begin, x] 是合法区间
return x;
return 23333333;
}
void work() {
int ans = 23333333;
for(int l=1; l<=n; l++) {
int r = findRight(l);
ans = min(ans, r - l + 1);
}
cout << ans << endl;
}
int main() {
input();
init();
work();
return 0;
}
但是其实这道题有线性( O ( 1 ) O(1) O(1))的做法.
定义指针l和r,一开始都指向1,如果cursum<s就将r向右移,直到cursum>=s为止,统计答案,再将l向右移。
这就是Two-pointers算法。
因为总体只移动了n次,所以时间复杂度为 O ( n ) O(n) O(n)。
#include <bits/stdc++.h>
using namespace std;
int n, s;
int a[100005];
void input() {
cin >> n >> s;
for(int i=1; i<=n; i++)
cin >> a[i];
}
void work() {
int l = 1, r = 1, curSum = a[1]; // 初始
int ans = 23333333;
while(l <= n) {
// 现在要对当前的 l,找到对应的合法最近 r
while(r <= n && curSum < s) {
// 当前区间和不够 s,需要右移 r 以增加区间和
r++;
curSum += a[r];
}
// printf("[%d, %d]\n", l, r);
if(curSum >= s)
ans = min(ans, r-l+1);
// 已经统计完当前 l 的答案,l 右移
curSum -= a[l];
l++;
}
cout << ans << endl;
}
int main() {
input();
work();
return 0;
}
This article was written on 2022/8/5