分治学习笔记
分治及分治优化学习笔记
前言
这里的分治主要将的是普通分治技巧,cdq分治,线段树分治的应用,树上的点分治之类的可能会再开一个专题(主要是现在作者还不会),先把基础打好。
普通分治
分治主要分为两种——最值分治和中点分值,顾名思义,就是一个取一个区间的最大值/最小值,而一个是直接取中间点即可。分治的方法一般放在 n∑i=1n∑j=if(i,j) 这样的式子上。可以从 O(n2) 的时间复杂度优化到 O(nlogn) 的时间复杂度。
以一道例题举例:E - Yet Another Sigma Problem (atcoder.jp)
这道题求的就是两个字符串的最长前缀。
而我们发现排序不影响结果,先排个序。
我们发现刚好,排完序之后,因为只有首字母相同的才可以有贡献,我们发现有贡献的都连在了一起,这样是不是就可以直接分治了呢?
直接对于第 k
个相同的区间进行分治每一次往前多推进一个字符。
代码也是非常简单。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3E5+5;
int n;
string s[N];
int solve(int l,int r,int k){
if(l>=r)return 0;
int p=l;
while(p<=r&&s[p].size()<=k)p++;
int res=0;
for(int i=p+1;i<=r;i++){
if(s[i][k]==s[p][k])continue;
res+=solve(p,i-1,k+1)+(i-p)*(i-p-1)/2;
p=i;
}
res+=solve(p,r,k+1)+(r-p+1)*(r-p)/2;
return res;
}
signed main(){
scanf("%lld",&n);
for(int i=1;i<=n;i++)
cin>>s[i];
sort(s+1,s+n+1);
cout<<solve(1,n,0)<<endl;
return 0;
}
这题相对来说还是很简单的,充其量只能算是一道分治的入门题。
[cf Special Segments of Permutation](Special Segments of Permutation)
这题我们很容易想到可以使用分治的方法来写,注意到这里的整数都两两不同,我们分别枚举最大值所在的位置,如果最大值所在的位置在右边,那我们找一下左边有没有 mx−pr ,反之亦然。
但是这里需要再找到这个点的时候判断一下我们枚举的左边大或右边大是否成立即可。
具体代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N=2E5+5;
int n,a[N],id[N],mx[N];
int solve(int l,int r){
if(l>=r)return 0;
int mid=l+r>>1;
int res=solve(l,mid)+solve(mid+1,r);
mx[mid]=a[mid];
for(int i=mid-1;i>=l;i--)
mx[i]=max(mx[i+1],a[i]);
mx[mid+1]=a[mid+1];
for(int i=mid+2;i<=r;i++)
mx[i]=max(mx[i-1],a[i]);
for(int i=mid+1;i<=r;i++){
int tmp=id[mx[i]-a[i]];
if(tmp>=l&&tmp<=mid&&mx[tmp]<=mx[i])res++;
}
for(int i=mid;i>=l;i--){
int tmp=id[mx[i]-a[i]];
if(tmp>=mid+1&&tmp<=r&&mx[tmp]<=mx[i])res++;
}
return res;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",a+i);
id[a[i]]=i;
}
cout<<solve(1,n);
return 0;
}
但是这也太简单了吧,分治也太简单了,吗?
这两题的分治还是很好看出来的,但是如何解决如何分治的问题呢?
我们发现这里有最大值和最小值和区间长度,这也太难求了吧。
但是其实确实很难不算太难,只要把该求的都求出来,思路清晰就不难求。
首先这样的题在分治的时候需要用一个"三指针"的方法,一个指针是从中间往右,其余两个分别表示左边的最大值和最小值的一个下标,每当右指针移动的时候,这两个指针相继移动,去计算收益。
总的写起来还是很难的,虽然码量上不是很大,但是思维量有一点大,稍微粗心一下,就要重新思考。有点麻烦。
cdq分治
cdq分治
主要是来解决三维偏序及其延伸题目的,这个在树状数组的专题上已经展示过了。这里也再放一遍模板。
void cdq(int l,int r) {
if(l==r)return;
int mid=l+r>>1;
cdq(l,mid),cdq(mid+1,r);
for(int i=l,L=l,R=mid+1; i<=r; i++) {
if(R>r||a[L].y >=a[R].y&&L<=mid) {
u[i]=a[L++];//与归并不同的地方
if(!u[i].id)bit.add(u[i].x,1);
} else {
u[i]=a[R++];
if(u[i].id)cnt[u[i].id]+=bit.query(u[i].x);//与归并不同的地方
}
}
for(int i=l; i<=r; i++)a[i]=u[i];
bit.clear();
}
主要是在三个维度进行比较来得出答案的,一般写三维偏序会在序列上搞两个数据,其一是原来的三个他给你的数据,其二是拿来比较的数据,有时这两个是一样的,但是分开写会清晰一点。
这里就放一道例题:Problem - 762E - Codeforces
这题的话放在三维偏序上也是比较难的一道题,也是道练三维偏序的好题。
很明显可以列出式子:∣fi−fj∣≤k,xi−xj≤min(ri,rj)
看到像绝对值,最小值这样的运算,尽量用分讨去除。
比如像第一个,我们就可以理解在$ [f_i-k,f_i+k ]$ 这个区间里选择,这个可以放在第三关键字在树状数组解决,而第二个式子可以在 cdq
内部在比较 xi,xj 即可,所以第一关键字是 r 其次是 x, 再是 f
代码如下:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+5;
int n,k,cnt,ans,b[N];
struct node {
int x,y,z,ll,rr;
bool operator <(const node &a)const {
if(x!=a.x)return x>a.x;
if(y!=a.y)return y<a.y;
return z<a.z;
}
} a[N];
struct B_tree {
int c[N],t[N],T;
void clear() {
T++;
}
inline int lowbit(int x) {
return x&-x;
}
inline void add(int x,int v) {
while(x<=cnt) {
if(t[x]==T)c[x]+=v;
else t[x]=T,c[x]=v;
x+=lowbit(x);
}
}
inline int query(int x) {
int res=0;
while(x) {
if(t[x]==T)res+=c[x];
x-=lowbit(x);
}
return res;
}
inline int query(int l,int r) {
return query(r)-query(l-1);
}
} bit;
bool cmp(node a,node b){
if(a.y !=b.y)return a.y<b.y;
if(a.z!=b.z )return a.z<b.z;
return a.x >b.x;
}
void cdq(int l,int r) {
if (l>=r) return;
int mid=l+r>>1;
cdq(l,mid),cdq(mid+1,r);
sort(a+l,a+mid+1,cmp);
sort(a+mid+1,a+r+1,cmp);
int L=l,R=l-1;
for(int i=mid+1; i<=r; i++) {
while(L<=mid&&a[i].y-a[L].y >k) {
bit.add(a[L].z ,-1);
L++;
}
while(R<mid&&a[i].y+k>=a[R+1].y ) {
R++;
bit.add(a[R].z,1);
}
ans+=bit.query(a[i].ll ,a[i].rr );
}
bit.clear();
//sort(a+l,a+r+1,cmp);
}
signed main() {
scanf("%lld%lld",&n,&k);
for(int i=1,x,f,r; i<=n; i++) {
scanf("%lld%lld%lld",&x,&r,&f);
///////x r f
a[i]= {r,f,x,max(0ll,x-r), x +r };
b[++cnt]=a[i].z;
b[++cnt]=a[i].ll ;
b[++cnt]=a[i].rr ;
}
sort(b+1,b+cnt+1);
cnt=unique(b+1,b+cnt+1)-b-1;
for(int i=1; i<=n; i++) {
a[i].ll =lower_bound(b+1,b+cnt+1,a[i].ll )-b;
a[i].rr =lower_bound(b+1,b+cnt+1,a[i].rr )-b;
a[i].z =lower_bound(b+1,b+cnt+1,a[i].z )-b;
}
//cout<<cnt<<endl;
sort(a+1,a+n+1);
bit.clear() ;
cdq(1,n);
printf("%lld",ans);
return 0;
}
线段树分治
这位更是重量级选手,线段树分治主要是对一些区间贡献的操作服务的,很多题目用线段树分治非常得心应手。
线段树分治的主要思路就是把一个区间分成跟线段树一样的一块块,然后再最后 dfs
下传贡献。
这里也提供一道例题:Problem - F - Codeforces
这里这题其实不是很难,我们发现就是算边的贡献就可以了,这里每一个颜色的一个小块都会给总体的贡献 sz∗(sz−1)/2 的。
可以把所有颜色看做一个区间,这里每加入一条边在 [1,c−1] 和 [c+1,n] 这些点上有贡献,在最后 dfs
时,直接用一个可撤销的 dsu
把这个区间上有贡献的都连起来,把每个小块的贡献加起来,输出即可。
#include <bits/stdc++.h>
#define pii pair<int, int>
#define ls p << 1
#define rs p << 1 | 1
using namespace std;
const int N = 5E5 + 5;
int n, fa[N], siz[N];
long long ans;
vector<pii> g[N << 2], h[N << 2];
int find(int x) {
while (fa[x] != x) x = fa[x];
return x;
}
void change(int p, int l, int r, int L, int R, pii tmp) {
if (L <= l && r <= R) {
g[p].push_back(tmp);
return;
}
int mid = (l + r >> 1);
if (L <= mid)
change(ls, l, mid, L, R, tmp);
if (R > mid)
change(rs, mid + 1, r, L, R, tmp);
}
void dfs(int p, int l, int r) {
// cout<<l<<" "<<r<<endl;
for (pii &tmp : g[p]) {
tmp.first = find(tmp.first);
tmp.second = find(tmp.second);
if (siz[tmp.first] > siz[tmp.second])
swap(tmp.first, tmp.second);
fa[tmp.first] = tmp.second;
siz[tmp.second] += siz[tmp.first];
}
if (l == r)
for (pii tmp : h[l]) {
ans += 1ll * siz[find(tmp.first)] * siz[find(tmp.second)];
}
else {
int mid = (l + r >> 1);
dfs(ls, l, mid), dfs(rs, mid + 1, r);
}
reverse(g[p].begin(), g[p].end());
for (pii tmp : g[p]) {
fa[tmp.first] = tmp.first;
siz[tmp.second] -= siz[tmp.first];
}
}
signed main() {
scanf("%d", &n);
for (int i = 1, u, v, c; i < n; i++) {
scanf("%d%d%d", &u, &v, &c);
h[c].push_back({ u, v });
if (c > 1)
change(1, 1, n, 1, c - 1, { u, v });
if (c < n)
change(1, 1, n, c + 1, n, { u, v });
}
for (int i = 1; i <= n; i++) siz[fa[i] = i] = 1;
dfs(1, 1, n);
cout << ans << endl;
return 0;
}
小结
分治主要还是考察从整体到局部的一种思想,和对题目透彻的了解。多加练习才能把一些技巧运用。