普及分治(那自然是未完成版)


概览

序列 时间轴
普通分治 分治 点分治 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\) 两种。

\[\sum_{i=l}^p \sum_{j=p}^r (j-i+1) = \sum_{i=l}^p \Big[ (1-i)(r-p+1) + \sum_{j=1}^r j - \sum_{j=1}^{p-1} j \Big] \]

反正化简完可以 \(O(1)\) 算, 在此不再赘述。
按照区间最大值分治, 复杂度为 \(O(n)\)


例题3
给定长度为 \(n\) 的序列, 区间的贡献为区间最大值乘以区间最小值乘以区间长度, 求所有区间的贡献和。
solution
按中点分治, 计算区间 \([l,r]\) 的经过 \(mid\) 的子区间的贡献和。
可以做到 \(O(n \log n)\), 原题是 bzoj 3745 , 具体做法可以随便上网上找篇博客, 比如这篇, 或者这篇
似乎还有 \(CDQ\) 分治的做法。


树分治

序列分治在树上的拓展 (maybe) 。
用于解决树上简单路径的统计问题。


这部分的参考资料

1.《算法竞赛进阶指南》
2
3
4
5
6


点分治

POJ1741

给树指定一个根节点 \(rt\), 则这棵树里的简单路径分为两类 :

  1. 路径上的点包含 \(rt\)
  2. 路径上的不点包含 \(rt\)

第二类路径可以递归处理, 一个核心问题是第一类路径的计算。
这就是点分治的最重要的思想了, 然而点分治的难度不止如此。

一般来说, 对于第一类路径的计算要有一个固定的复杂度, 剩下的影响复杂度的因素就是分治的递归层数。若每次选择树的重心为根节点递归, 则总的递归层数不超过 \(O(\log 整棵树的节点个数)\)

有一篇 值得一读的博客, 感兴趣的可以看看。

本题的几种做法
不同做法的区别是第一类路径的计算方式。

首先是直接统计的方法。
逐个扫描不同子树内的节点, 在数据结构里查询, 扫描完后将整个子树内的有关数据插入到数据结构里。

难写, 不写了。

一种优化双指针的算法。我从《进阶指南》上看到它。
d[x]x 到根节点的距离, b[x] 记录 x 属于根节点的哪颗子树。
把树中所有点放进数组 a , 按照 d 值排序。
用双指针 lr 分别从前端和后端开始扫描 a 数组, 过程中要始终满足 d[a[l]] + d[a[r]] <= K, 显然随着 l 的增加, r 的位置是单调的, 整个过程复杂度较低。
扫描的同时用数组 cnt[s] 维护在 l+1r 之间满足 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 << '\n';
}

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] << '\n';
	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] << '\n';
	
	return 0;
}

例题

整体二分

参考文献:

1
2
3
4
5
6

posted @ 2020-08-20 08:31  xwmwr  阅读(152)  评论(0编辑  收藏  举报