CDQ
cdq指 IOI2008 金牌得主陈丹琦,%%%%%%
佩服
陈丹琦的论文
正题
CDQ分治主要有三大用武之地:
1.降维
2.优化DP
3.在线转离线
降维
对于一个三维的问题,我们可以用其降为两维
经典的例子就是三维偏序`
这里我们拿另一道题举例动态逆序对
仔细观察可以发现,若设第一个元素消失的时间t为n,并依次递减,那么一个元素有三个值(t,id,a)
这就构成了三维,而对于任意点对(i,j),只要满足
t
i
<
t
j
且
i
d
i
>
i
d
j
且
a
i
<
a
j
t_i<t_j 且id_i>id_j且a_i<a_j
ti<tj且idi>idj且ai<aj
或者
t
i
<
t
j
且
i
d
i
<
i
d
j
且
a
i
>
a
j
t_i<t_j 且id_i<id_j且a_i>a_j
ti<tj且idi<idj且ai>aj
就可以令答案加1.
实现如下:
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=1e5+10;
struct AC{int m,v,d,id,t;}e[N<<1];
int n,m,tot;
int pos[N],a[N],c[N];ll ans[N];
bool cmp1(AC x,AC y){return x.d<y.d;}
void add(int x,int k){while (x<=n)c[x]+=k,x+=(x&(-x));}
int query(int x){int su=0;while(x)su+=c[x],x-=(x&(-x));return su;}
void cdq(int l,int r){
if (l==r) return;
int mid=(l+r)>>1,j=l;
cdq(l,mid),cdq(mid+1,r);
sort(e+l,e+mid+1,cmp1);
sort(e+mid+1,e+r+1,cmp1);
for (int i=mid+1;i<=r;++i){
while (j<=mid&&e[j].d<=e[i].d)add(e[j].v,e[j].m),++j;
ans[e[i].id]+=e[i].m*(query(n)-query(e[i].v));
}
for (int i=l;i<j;++i) add(e[i].v,-e[i].m);
j=mid;
for (int i=r;i>mid;--i){
while (j>=l&&e[j].d>=e[i].d)add(e[j].v,e[j].m),--j;
ans[e[i].id]+=e[i].m*query(e[i].v-1);
}
for (int i=mid;i>j;--i) add(e[i].v,-e[i].m);
}
int main(){
scanf("%d%d",&n,&m);
for (int i=1;i<=n;++i)scanf("%d",&a[i]),pos[a[i]]=i,e[++tot]=(AC){1,a[i],i,0,tot};
for (int i=1,x;i<=m;++i)scanf("%d",&x),e[++tot]=(AC){-1,x,pos[x],i,tot};
cdq(1,tot);
for (int i=1;i<=m;++i) ans[i]+=ans[i-1];
for (int i=0;i<m;++i) printf("%lld\n",ans[i]);
}
优化DP
大多数情况下,CDQ分治优化的是1D/1D 动态
1D/1D 动态规划指的是一类特定的 DP 问题,该类题目的特征是 DP 数组是一维的,转移是
O
(
n
)
O(n)
O(n) 的。如果条件良好的话,有时可以通过 CDQ 分治来把它们的时间复杂度由
O
(
n
2
)
O(n^2)
O(n2)降至
O
(
n
log
2
n
)
O(n\log^2n)
O(nlog2n)
例题:拦截导弹
很容易想出DP式:
f
i
=
1
+
f
j
(
h
i
<
h
j
,
v
i
<
v
j
)
f_i=1+f_j(h_i<h_j , v_i<v_j)
fi=1+fj(hi<hj,vi<vj)
所以得到
O
(
n
2
)
O(n^2)
O(n2)的做法
#include <bits/stdc++.h>
using namespace std;
const int N = 5e4 + 7;
int n, f[N], ans;
struct st {
int h, v;
} rk[N];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d%d", &rk[i].h, &rk[i].v);
for (int i = 1; i <= n; i++) {
for (int j = 1; j < i; j++) {
if (rk[i].h <= rk[j].h && rk[i].v <= rk[j].v) {
f[i] = max(f[i], f[j]);
}
}
f[i]++;
ans = max(ans, f[i]);
}
printf("%d\n", ans);
for (int i = 1; i <= n; i++)
printf("0.000000 ");
}
用CDQ可以优化
我们发现
f
j
f_{j}
fj 转移到
f
i
f_{i}
fi 这种转移关系也是一种点对间的关系,所以我们用类似 CDQ 分治处理点对关系的方式来处理它。
这个转移过程相对来讲比较套路。假设现在正在处理的区间是 (l,r),算法流程大致如下:
1.如果 l=r,说明
f
r
f_{r}
fr 值已经被计算好了。直接令
f
r
f_{r}
fr++ 然后返回即可;
2.递归使用 solve(l,mid);
3.处理所有
l
≤
j
≤
m
i
d
,
m
i
d
+
1
≤
i
≤
r
l \leq j \leq mid,mid+1 \leq i \leq r
l≤j≤mid,mid+1≤i≤r 的转移关系;
4.递归使用 solve(mid+1,r)。
#include <bits/stdc++.h>
#define FOR(a,b,c) for(int a=b;a<=c;a++)
using namespace std;
const int N = 1e5 + 10;
struct Node {
int id, x, y;
bool operator<(const Node &rhs)const {
return x < rhs.x || (x == rhs.x && y < rhs.y);
}
} q[N], t[N];
bool cmp(const Node &a, const Node &b) {
return a.id < b.id;
}
int f[2][N];
double g[2][N], ans[N];
int hh[N], tot, n;
int read() {
char c = getchar();
int x = 0;
int ff = 1;
while (!isdigit(c)) {
if (c == '-')
ff = -1;
c = getchar();
}
while (isdigit(c))
x = x * 10 + c - '0', c = getchar();
return x * ff;
}
int t_f[N], tag[N], T;
double t_g[N];
void add(int x, int ff, double gg) {
for (; x <= tot; x += x & -x) {
if (tag[x] != T) {
tag[x] = T;
t_f[x] = 0;
t_g[x] = 0;
}
if (ff > t_f[x]) {
t_f[x] = ff;
t_g[x] = gg;
} else if (ff == t_f[x])
t_g[x] += gg;
}
}
void query(int x, int &mx, double &sum) {
mx = 0;
sum = 0.0;
for (; x; x -= x & -x)
if (tag[x] == T) {
if (t_f[x] > mx) {
mx = t_f[x];
sum = t_g[x];
} else if (t_f[x] == mx)
sum += t_g[x];
}
}
void solve(int l, int r, int ty) {
if (l == r) {
if (!f[ty][l]) {
f[ty][l] = 1;
g[ty][l] = 1;
}
return ;
}
int mid = (l + r) >> 1;
int l1 = l, l2 = mid + 1, i, j, cnt = 0;
for (i = l; i <= r; i++) {
if (q[i].id <= mid)
t[l1++] = q[i];
else
t[l2++] = q[i];
}
memcpy(q + l, t + l, sizeof(Node) * (r - l + 1));
solve(l, mid, ty);
T++;
sort(q + mid + 1, q + r + 1);
for (i = mid + 1, j = l; i <= r; i++) {
int id;
for (; j <= mid && q[j].x <= q[i].x; j++) {
id = q[j].id;
cnt++;
add(q[j].y, f[ty][id], g[ty][id]);
}
int mx;
double sum;
query(q[i].y, mx, sum);
id = q[i].id;
if (mx > 0) {
if (mx + 1 > f[ty][id]) {
f[ty][id] = mx + 1;
g[ty][id] = sum;
} else if (mx + 1 == f[ty][id]) {
g[ty][id] += sum;
}
}
}
solve(mid + 1, r, ty);
l1 = l, l2 = mid + 1;
int now = l;
while (l1 <= mid || l2 <= r) {
if (l2 > r || l1 <= mid && q[l1] < q[l2])
t[now++] = q[l1++];
else
t[now++] = q[l2++];
}
memcpy(q + l, t + l, sizeof(Node) * (r - l + 1));
}
int main() {
n = read();
int mxx = 0;
FOR(i, 1, n) {
q[i].x = read(), q[i].y = read();
hh[i] = q[i].y;
mxx = max(mxx, q[i].x);
}
sort(hh + 1, hh + n + 1);
tot = unique(hh + 1, hh + n + 1) - hh - 1;
FOR(i, 1, n)
q[i].y = lower_bound(hh + 1, hh + tot + 1, q[i].y) - hh;
reverse(q + 1, q + n + 1);
FOR(i, 1, n) q[i].id = i;
solve(1, n, 0);
sort(q + 1, q + n + 1, cmp);
reverse(q + 1, q + n + 1);
FOR(i, 1, n)
q[i].x = mxx - q[i].x + 1, q[i].y = tot - q[i].y + 1,
q[i].id = i;
solve(1, n, 1);
int mx = 0;
double sum = 0;
FOR(i, 1, n) {
int tmp = f[0][i];
if (tmp > mx) {
mx = tmp;
sum = g[0][i] * g[1][n - i + 1];
} else if (tmp == mx)
sum += g[0][i] * g[1][n - i + 1];
}
printf("%d\n", mx);
for (int i = n; i; i--) {
if (f[0][i] + f[1][n - i + 1] - 1 == mx) {
ans[i] = (g[0][i] * g[1][n - i + 1]) / sum;
} else
ans[i] = 0;
}
for (int i = n; i > 1; i--)
printf("%.5lf ", ans[i]);
printf("%.5lf", ans[1]);
return 0;
}
题外话
嘿嘿,越来越难了,对吧,没关系,还有更难的
将动态问题转化为静态问题
做法大概就是将所有操作离线,在进行询问操作时会发现,会影响其的修改操作已经全部解决了
思路简单,却极其难想
[HNOI2010] 城市建设
思路:
假设我们正在构造
(
l
,
r
)
(l,r)
(l,r) 这段区间的最小生成树边集,并且我们已知它父亲最小生成树的边集。我们将在
(
l
,
r
)
(l,r)
(l,r) 这段区间中发生变化的边分别赋与
+
∞
+ \infty
+∞和
−
∞
-\infty
−∞ 的边权,并各跑一边 kruskal,求出在最小生成树里的那些边。
对于一条边来讲:
如果最小生成树里所有被修改的边权都被赋成了
+
∞
+\infty
+∞,而它未出现在树中,则证明它不可能出现在
(
l
,
r
)
(l,r)
(l,r) 这些询问的最小生成树当中。所以我们仅仅在 (l,r) 的边集中加入最小生成树的树边。
如果最小生成树里所有被修改的边权都被赋成了
−
∞
-\infty
−∞,而它出现在树中,则证明它一定会出现
(
l
,
r
)
(l,r)
(l,r) 这段的区间的最小生成树当中。这样的话我们就可以使用并查集将这些边对应的点缩起来,并且将答案加上这些边的边权。
#include<bits/stdc++.h>
#define pb push_back
#define mk make_pair
using namespace std;
typedef long long ll;
const int N=5e4+7;
int n,m,q,fa[N],sz[N],tag,ta[N];
ll ans[N];
struct edge{
int u,v,w;
bool friend operator<(edge a,edge b) { return a.w<b.w; }
}E[N];
struct Query{ int id, d; }qu[N];
bool cmp(int x,int y){ return E[x]<E[y]; }
vector<pair<int *,int> > buc;
void push(int &x,int v){
buc.pb(mk(&x,x));
x=v;
}
void clear(int t){
while(buc.size()>t) {
*buc.back().first=buc.back().second,buc.pop_back();
}
}
int find(int k){ return k==fa[k]?k:find(fa[k]); }
void unite(int x,int y){
x=find(x),y=find(y);
if(x==y) return;
if(sz[x]>sz[y]) swap(x,y);
push(fa[x],y);push(sz[y],sz[x]+sz[y]);
}
void work(vector<int> &e,int l,int r,ll &val){
int st=buc.size();
vector<int> tmp;
sort(e.begin(),e.end(),cmp);
for(int i=l;i<=r;i++) unite(E[qu[i].id].u,E[qu[i].id].v);
for(int i:e){
if(ta[i]==tag) continue;
int r1=find(E[i].u),r2=find(E[i].v);
if(r1!=r2) {
unite(r1,r2); val+=E[i].w; tmp.pb(i);
}
}
clear(st);
for(int i:tmp) unite(E[i].u,E[i].v);
}
void del(vector<int> &e){
int st=buc.size();
vector<int> tmp;
sort(e.begin(),e.end(),cmp);
for(int i:e){
if(ta[i]==tag) {
tmp.pb(i);
continue;
}
int r1=find(E[i].u),r2=find(E[i].v);
if(r1!=r2) {
unite(r1,r2); tmp.pb(i);
}
}
clear(st);
e.swap(tmp);
}
void solve(int l ,int r ,vector<int> e ,ll delta ) {
if(l==r) E[qu[l].id].w=qu[l].d;
int st=buc.size();
if(l==r){
sort(e.begin(),e.end(),cmp );
for(int i:e){
int r1=find(E[i].u),r2=find(E[i].v);
if(r1==r2) continue;
unite(r1,r2);
delta+=E[i].w;
}
ans[l]=delta;
}else{
tag++;//记录动态边
for(int i=l;i<=r;i++) ta[qu[i].id]=tag;
work(e,l,r,delta);//找出必用边
del(e);//删去必不用边
int mid=(l+r)>>1;
solve(l,mid,e,delta); solve(mid+1,r,e,delta);
}
clear(st);
}
int main(){
scanf("%d%d%d",&n,&m,&q);
for(int i=1;i<=n;i++) fa[i]=i,sz[i]=1;
vector<int>ss;
for(int i=1;i<=m;i++) {
scanf("%d%d%d",&E[i].u,&E[i].v,&E[i].w);
ss.pb(i);
}
for(int i=1;i<=q;i++) scanf("%d%d",&qu[i].id,&qu[i].d);
solve(1,q,ss,0);
for(int i=1;i<=q;i++)
printf("%lld\n",ans[i]);
return 0;
}
题后话
路漫漫其修远兮,吾将上下而求索