概览
序列 | 树 | 时间轴 | |
---|---|---|---|
普通分治 | 分治 | 点分治 | CDQ分治 |
将过程建树 | 线段树 | 点分树 | 时间线段树 |
由于是普及分治, 没有数据结构(线段树、点分树和时间线段树)。
14年候选队的徐寅展写了时间线段树。
序列分治
用于计算序列上的问题
n 个数排序
用快速排序, 复杂度 (T(n) = 2T(dfrac{n}{2}) + O(n) = O(n log n))。
经典模型
给定一个长度为 n 的序列和区间 [l,r] 的贡献计算方式 f(l,r), 求所有区间的贡献和。
例题1
给定长度为 (n) 的序列, 求逆序对数。
solution
给区间 ([l,r]) 规定一个 (mid in [l,r]), 那么 ([l,r]) 的一对逆序对 ((a,b)) 可以分为 (a in [l,mid], ; b in [mid+1,r]) 和 (a、b) 在 (mid) 同一侧两类, 用分治计算, 有个经典算法是在归并排序的过程中顺便计算, 时间复杂度为 (O(n log n))。
例题2
给定长度为 (n) 的序列, 区间的贡献为区间最大值乘以区间长度, 求所有区间的贡献和。
solution
假设区间 ([l,r]) 的最大值的位置为 (p), 则这个区间的所有子区间可以分为包括 (p) 和不包括 (p) 两种。
反正化简完可以 (O(1)) 算, 在此不再赘述。
按照区间最大值分治, 复杂度为 (O(n))。
例题3
给定长度为 (n) 的序列, 区间的贡献为区间最大值乘以区间最小值乘以区间长度, 求所有区间的贡献和。
solution
按中点分治, 计算区间 ([l,r]) 的经过 (mid) 的子区间的贡献和。
可以做到 (O(n log n)), 原题是 bzoj 3745 , 具体做法可以随便上网上找篇博客, 比如这篇, 或者这篇。
似乎还有 (CDQ) 分治的做法。
树分治
序列分治在树上的拓展 (maybe) 。
用于解决树上简单路径的统计问题。
这部分的参考资料
点分治
给树指定一个根节点 (rt), 则这棵树里的简单路径分为两类 :
- 路径上的点包含 (rt)
- 路径上的不点包含 (rt)
第二类路径可以递归处理, 一个核心问题是第一类路径的计算。
这就是点分治的最重要的思想了, 然而点分治的难度不止如此。
一般来说, 对于第一类路径的计算要有一个固定的复杂度, 剩下的影响复杂度的因素就是分治的递归层数。若每次选择树的重心为根节点递归, 则总的递归层数不超过 (O(log 整棵树的节点个数))。
有一篇 值得一读的博客, 感兴趣的可以看看。
本题的几种做法
不同做法的区别是第一类路径的计算方式。
首先是直接统计的方法。
逐个扫描不同子树内的节点, 在数据结构里查询, 扫描完后将整个子树内的有关数据插入到数据结构里。
难写, 不写了。
一种优化双指针的算法。我从《进阶指南》上看到它。
设 d[x] 为 x 到根节点的距离, b[x] 记录 x 属于根节点的哪颗子树。
把树中所有点放进数组 a , 按照 d 值排序。
用双指针 l 、 r 分别从前端和后端开始扫描 a 数组, 过程中要始终满足 d[a[l]] + d[a[r]] <= K, 显然随着 l 的增加, r 的位置是单调的, 整个过程复杂度较低。
扫描的同时用数组 cnt[s] 维护在 l+1 与 r 之间满足 b[a[i]] = s 的位置 i 的个数。
于是, 路径的一端为 a[l] 时, 满足题目要求的路径另一端的个数就是 r-l-cnt[b[a[l]]]
这个算法对于普通的双指针优化, 达到了去重和去不合法的效果, 很妙而且优美。
#include<bits/stdc++.h>
using namespace std;
const int N = 10006;
int n,k,s[N],Ans;
int ct, hd[N], nt[N<<1], vr[N<<1], vl[N<<1];
bool w[N], v[N];
int ans, pos;
int tot, a[N], b[N], d[N], cnt[N];
void dfs_find(int S,int x) {
v[x] = 1;
s[x] = 1;
int max_part = 0;
for(int i=hd[x];i;i=nt[i]) {
int y=vr[i];
if(w[y] || v[y]) continue;
dfs_find(S,y);
s[x] += s[y];
max_part = max(max_part, s[y]);
}
max_part = max(max_part, S - s[x]);
if(ans > max_part) {
ans = max_part;
pos = x;
}
}
void dfs(int x) {
v[x] = 1;
for(int i=hd[x];i;i=nt[i]) {
int y = vr[i], z = vl[i];
if(w[y] || v[y]) continue;
++cnt[b[a[++tot]=y]=b[x]];
d[y] = d[x] + z;
dfs(y);
}
}
bool cmp(int i,int j) {
return d[i] < d[j];
}
void work(int S, int x) {
memset(v,0,sizeof v);
ans = S;
dfs_find(S, x);
memset(v,0,sizeof v);
memset(d,0,sizeof d);
memset(cnt, 0, sizeof cnt);
w[a[tot=1] = b[pos] = pos] = 1;
++cnt[pos];
for(int i=hd[pos];i;i=nt[i]) {
int y=vr[i], z=vl[i];
if(w[y] || v[y]) continue;
++cnt[a[++tot]=b[y]=y];
d[y] = z;
dfs(y);
}
sort(a+1,a+1+tot,cmp);
int l=1, r=tot;
--cnt[b[a[l]]];
while(l<r) {
while(d[a[l]]+d[a[r]] > k) --cnt[b[a[r--]]];
Ans += r-l-cnt[b[a[l]]];
--cnt[b[a[++l]]];
}
int now = pos;
for(int i=hd[now];i;i=nt[i]) {
int y=vr[i];
if(w[y]) continue;
work(s[y], y);
}
}
void ad(int x,int y,int z) {
vr[++ct] = y;
vl[ct] = z;
nt[ct] = hd[x];
hd[x] = ct;
}
void Tree() {
ct = 0;
memset(hd,0,sizeof hd);
for(int i=1;i<n;++i) {
int x,y,z;
scanf("%d%d%d", &x,&y,&z);
++x; ++y;
ad(x,y,z);
ad(y,x,z);
}
Ans = 0;
memset(w, 0, sizeof w);
work(n, 1);
cout << Ans << '
';
}
int main() {
while(cin>>n>>k&&n&&k) Tree();
return 0;
}
双指针+容斥的做法。由 PinkRabbit 提出。
首先去掉简单路径的限制统计到根节点距离和 (le K) 的点对数量, 然后用减法原理去掉非简单路径的部分, 具体地, 在根节点的每颗子树内都用一次上述统计方法。
这个算法也很优美。
#include<bits/stdc++.h>
using namespace std;
int main() {
return 0;
}
[IOI2011]Race
点分治, 问题转化为计算有根树过根节点的路径。
开个桶, 问题不大。
TLE 成 80, 自闭了orz
#include<bits/stdc++.h>
using namespace std;
const int N = 200003;
int n,k,s[N],Ans;
int ct, hd[N], nt[N<<1], vr[N<<1], vl[N<<1];
bool v[N], w[N];
int ans, pos;
int d[N], num[N], min_num[1000001], dl;
void dfs_find(int S,int x) {
v[x] = 1;
s[x] = 1;
int max_part = 0;
for(int i=hd[x];i;i=nt[i]) {
int y = vr[i];
if(w[y] || v[y]) continue;
dfs_find(S,y);
s[x] += s[y];
max_part = max(max_part, s[y]);
}
max_part = max(max_part, S-s[x]);
if(ans > max_part) {
ans = max_part;
pos = x;
}
}
void dfs(int x, int D, int Num) {
if(D > k) return;
v[x] = 1;
d[++dl] = D, num[dl] = Num;
for(int i=hd[x];i;i=nt[i]) {
int y = vr[i];
if(w[y] || v[y]) continue;
dfs(y, D+vl[i], Num+1);
}
}
void work(int S,int x) {
memset(v,0,sizeof v);
ans = S;
dfs_find(S, x);
w[pos] = 1;
memset(v,0,sizeof v);
dl=0;
min_num[0] = 0;
for(int i=hd[pos];i;i=nt[i]) {
int y = vr[i], z = vl[i];
if(w[y] || v[y]) continue;
int pdl = dl;
dfs(y, z, 1);
for(int j=pdl+1;j<=dl;++j) Ans = min(Ans, min_num[k-d[j]]+num[j]);
for(int j=pdl+1;j<=dl;++j) min_num[d[j]] = min(min_num[d[j]], num[j]);
}
for(int i=1;i<=dl;++i) min_num[d[i]] = 1e9;
for(int i=hd[pos];i;i=nt[i]) {
int y = vr[i];
if(w[y]) continue;
work(s[y], y);
}
}
void ad(int x,int y,int z) {
vr[++ct] = y;
vl[ct] = z;
nt[ct] = hd[x];
hd[x] = ct;
}
int main() {
cin >> n >> k;
Ans = n+1;
for(int i=1;i<n;++i) {
int x,y,z;
scanf("%d%d%d", &x,&y,&z);
++x; ++y;
ad(x,y,z);
ad(y,x,z);
}
memset(min_num, 0x63, sizeof min_num);
min_num[0] = 0;
work(n, 1);
cout << (Ans==n+1 ? -1 : Ans);
return 0;
}
边分治
CDQ 分治(基本)
用 CDQ 写单点加区间查询
#include<bits/stdc++.h>
using namespace std;
const int N = 500055;
inline int read()
{
register int X=0;
register char ch=0,w=0;
while(ch<48||ch>57)ch=getchar(),w|=(ch=='-');
while(ch>=48&&ch<=57)X=X*10+(ch^48),ch=getchar();
return w?-X:X;
}
int n,m, a[N];
struct node{
int x,y,type;
// 1是修改, 2是减法, 3是加法
// x 是位置
// y 权 答案记在哪
} b[N*3+1], s[N*3+1];
int tn;
int Ans[N], q;
void CDQ(int l, int r) {
if(l==r) return;
int mid = (l+r)>>1;
CDQ(l,mid); CDQ(mid+1,r);
int p = mid+1, sum = 0, hd = l-1;
for(int i=l;i<=mid;++i) {
while(p<=r && b[p].x < b[i].x) {
s[++hd] = b[p];
if(b[p].type==2) Ans[b[p].y] -= sum;
if(b[p].type==3) Ans[b[p].y] += sum;
++p;
}
if(b[i].type==1) sum += b[i].y;
s[++hd] = b[i];
}
while(p<=r) {
s[++hd] = b[p];
if(b[p].type==2) Ans[b[p].y] -= sum;
if(b[p].type==3) Ans[b[p].y] += sum;
++p;
}
memcpy(b+l,s+l,sizeof(node)*(r-l+1));
}
int main() {
scanf("%d%d", &n,&m);
for(int i=1;i<=n;++i) {
b[++tn] = (node){i,read(),1};
}
for(int i=1, op;i<=m;++i) {
op = read();
if(op==1) {
op = read();
b[++tn] = (node){op,read(),1};
} else {
++q;
b[++tn] = (node){read()-1, q, 2};
b[++tn] = (node){read(), q, 3};
}
}
CDQ(1,tn);
for(int i=1;i<=q;++i) cout << Ans[i] << '
';
return 0;
}
三维偏序问题
陌上花开
先按第一维排序, 去重, 后面的不会对前面的产生贡献。
(CDQ), 要解决的就是修改都在询问前头的二位偏序。
由于前一半的第一维一定都小于后一半的第一维, 所以把前一半标记, 再按照第二维排序后, 套个树状数组即可解决问题, 这样的正确性可以保证。
#include<bits/stdc++.h>
using namespace std;
const int N = 100003;
int n,m,k;
struct node{
int x,y,z,w,op,ans;
} a[N], b[N];
int d[N];
int t[N*2+1];
void ad(int x,int v) {
for(;x<=k;x+=x&(-x)) t[x] += v;
}
void cl(int x,int v) {
for(;x<=k;x+=x&(-x)) t[x] -= v;
}
int ques(int x) {int res=0;
for(;x;x-=x&(-x)) res += t[x]; return res;
}
bool cmp2(node s1, node s2) {
return s1.y==s2.y ? (s1.x==s2.x ? s1.z<s2.z : s1.x<s2.x) : s1.y<s2.y;
}
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;i<=mid;++i) b[i].op=1;
for(int i=mid+1;i<=r;++i) b[i].op=2;
sort(b+l,b+r+1,cmp2);
for(int i=l;i<=r;++i) {
if(b[i].op==1) ad(b[i].z, b[i].w);
else b[i].ans += ques(b[i].z);
}
for(int i=l;i<=r;++i) if(b[i].op==1) cl(b[i].z,b[i].w);
}
bool cmp(node s1, node s2) {
return s1.x==s2.x ? (s1.y==s2.y ? s1.z<s2.z : s1.y<s2.y) : s1.x<s2.x;
}
int main() {
cin >> n >> k;
for(int i=1;i<=n;++i) {
scanf("%d%d%d", &a[i].x, &a[i].y, &a[i].z);
a[i].w = 1;
}
sort(a+1, a+1+n, cmp);
for(int i=1;i<=n;++i) {
if(a[i].x==a[i+1].x && a[i].y==a[i+1].y && a[i].z==a[i+1].z) a[i+1].w += a[i].w;
else b[++m] = a[i];
}
cdq(1,m);
for(int i=1;i<=m;++i) d[b[i].ans+b[i].w-1] += b[i].w;
for(int i=0;i<n;++i) cout << d[i] << '
';
return 0;
}