CDQ分治题目小结
CDQ分治属于比较特殊的一类分治,许多问题转化为这类分治的时候,时空方面都会有很大节省,而且写起来没有这么麻烦。
这类分治的特殊性在于分治的左右两部分的合并,作用两部分在合并的时候作用是不同的,比如,通过左半部分的影响来更新右半部分,所以分治开始前都要按照某一个关键字排序,然后利用这个顺序,考虑一个区间[l, r]的两部分间的影响。感觉说的太多,还是不如具体题目分析,而且题目也不尽相同,记住几句话是没什么用的。
练习地址:
http://vjudge.net/contest/view.action?cid=55322#overview
Problem A HYSBZ 3110 K大数查询
这个是一个很经典的树套树题目,看别人博客发现CDQ分治能够很好处理。
题意:有n个位置编号1~n,m个操作,每个操作两种类型:1 a b c 表示将第a~b个位置之间的每个位置插入一个数c;2 a b c 查询第a~b个位置之间的所有数中,第c大的数。
范围:
N,M<=50000,N,M<=50000
a<=b<=N
1操作中abs(c)<=N
2操作中abs(c)<=Maxlongint
分析:
按照CDQ分治的做法,是答案当做关键字来分治,由于答案最终在-n~n之间,这里首先需要一个转化,将区间第c大变成第c小,只需要将每个数变成n-c+1。
对于这类操作类的题目 ,CDQ分治的做法首先要保证的是操作的顺序,接下来以答案为关键字,例如询问结果在L~R之间的操作2,分成两部分递归L~m,m+1~R处理,#11对于操作1如果添加的数<=m,则加入到相应的位置区间;#12否则说明操作1影响答案在右半区间m+1~R的操作2。然后对于每个操作2查询当前位置区间有多少个数,表示该区间<=m已经有多少个数(#21),如果(#22)数目tmp > c (查询数目),说明答案应该在m+1~R,否则在L~m。然后将操作1中影响答案在左半部分的(编号#11)和操作2中答案在左半部分的(#21)集中在一起左半部分,剩下的集中在右半部分。然后递归处理答案在左半部分和右半部分的。每次进行子区间的递归时都将操作分成了2部分,表示不同区间被对应不同的操作。
具体成段增加一个值和查询某一段的和用到了树状数组,也可以用线段树,不过我觉得树状数组解法简洁有力,orz,
上一下原文树状数组链接
http://www.cnblogs.com/lazycal/archive/2013/08/05/3239304.html
我的代码:
bzoj好像不能用cout输出一个表达式,会RE!
1 /*Time 2014 08 31 , 19:26 2 3 */ 4 #include <bits/stdc++.h> 5 #define in freopen("solve_in.txt", "r", stdin); 6 #define bug(x) printf("Line %d : >>>>>>>\n", (x)); 7 8 using namespace std; 9 typedef long long LL; 10 const int maxn = 50000 + 100; 11 LL x1[maxn][2], x2[maxn][2], ans[maxn]; 12 int cnt; 13 14 struct Node 15 { 16 int l, r, type; 17 LL c; 18 int id; 19 } q[maxn]; 20 int rk[maxn], t1[maxn], t2[maxn]; 21 int n, m; 22 LL query(LL a[][2], int x) 23 { 24 LL res = 0; 25 for(; x > 0; x -= (x&(-x))) 26 { 27 if(a[x][0] == cnt) res += a[x][1]; 28 } 29 return res; 30 } 31 LL query(int l, int r) 32 { 33 return query(x1, l)*(r-l+1)+ (r+1)*(query(x1, r)-query(x1, l)) - (query(x2, r)-query(x2, l)); 34 } 35 void add(LL a[][2], int x, LL c) 36 { 37 for(; x <= n; x += ((-x)&x)) 38 { 39 if(a[x][0] == cnt) a[x][1] += c; 40 else a[x][0] = cnt, a[x][1] = c; 41 } 42 } 43 void add(int l, int r, int c) 44 { 45 add(x1, l, c); 46 add(x2, l, (LL)l*c); 47 add(x1, r+1, -c); 48 add(x2, r+1, (LL)(r+1)*(-c)); 49 } 50 void solve(int ll, int rr, int l, int r) 51 { 52 if(l > r) return; 53 if(ll == rr) 54 { 55 for(int i = l; i <= r; i++) 56 if(q[rk[i]].type == 2) 57 { 58 ans[rk[i]] = ll; 59 } 60 return; 61 } 62 int m1 = (ll+rr)>>1, m2 = (l+r)>>1; 63 cnt++; 64 t1[0] = t2[0] = 0; 65 for(int i = l; i <= r; i++) 66 { 67 if(q[rk[i]].type == 1) 68 { 69 if(q[rk[i]].c <= m1) 70 { 71 add(q[rk[i]].l, q[rk[i]].r, 1); 72 t1[++t1[0]] = rk[i]; 73 } 74 else 75 { 76 t2[++t2[0]] = rk[i]; 77 } 78 } 79 else 80 { 81 LL xx = query(q[rk[i]].l, q[rk[i]].r); 82 if(xx < (LL)q[rk[i]].c) 83 { 84 q[rk[i]].c -= xx; 85 t2[++t2[0]] = rk[i]; 86 } 87 else 88 { 89 t1[++t1[0]] = rk[i]; 90 } 91 } 92 } 93 m2 = l+t1[0]-1; 94 95 for(int i = l; i <= r; i++) 96 { 97 if(i <= m2) 98 { 99 rk[i] = t1[i-l+1]; 100 } 101 else 102 { 103 rk[i] = t2[i-m2]; 104 } 105 } 106 solve(ll, m1, l, m2); 107 solve(m1+1, rr, m2+1, r); 108 } 109 int main() 110 { 111 112 scanf("%d%d", &n, &m); 113 for(int i = 1; i <= m; i++) 114 { 115 rk[i] = i; 116 scanf("%d%d%d%lld", &q[i].type, &q[i].l, &q[i].r, &q[i].c); 117 if(q[i].type == 1) q[i].c = (LL)n-q[i].c+1; 118 q[i].id = i; 119 } 120 solve(1, 2*n+1, 1, m); 121 for(int i = 1; i <= m; i++) 122 { 123 if(q[i].type == 2) 124 { 125 printf("%d\n", n-ans[i]+1); 126 } 127 } 128 return 0; 129 }
Problem B HYSBZ 1492 货币兑换Cash
题意:一开始有S元现金,接下来共有N天,每天两种货币的价格分别为a[i],b[i],以及卖入时,ab货币的比列为r[i],问N天结束时最多能有多少现金。
分析:
最后一天结束时一定时将货币全部换成现金,那么第i天货币数目x[i], y[i],第i天最多持有的现金
f[i] = max{x[j]*a[i]+y[j]*b[i]|(j < i)},
y[j] = f[j]/(a[j]*r[j]+b[j]), x[j] = y[j]*r[j].
化简后f[i]/b[i] - x[j]*a[i]/b[i] = y[j],发现最优解便是使得f[i]/b[i]最大,也就是斜率为-a[i]/b[i]的斜率,截距最大。对于点(x[j], y[j])能够影响到之后的f[i], i >j,f[i]最优解一定落在前i-1天行成的凸壳上,那么怎么高效维护这个凸壳是问题的核心,与普通斜率优化不同的是这题的斜率与x均不会单调,所以事先将斜率排序,然后按照斜率递减的顺序来在凸壳上找最优解是可行的。因为斜率递减的话,切凸壳上点得到的截距会越来越大。然后就是维护以个凸壳,最终这个凸壳相邻两点斜率也要递减。那么每次递归结束时按照x[i]排序,方便下次维护生成凸壳。
代码:
http://vjudge.net/contest/viewSource.action?id=2724881
Problem C CodeForces 396C On Changing Tree
题解见这里
http://www.cnblogs.com/rootial/p/3948478.html
关键在于两个操作1的合并, 将树的叶子结点编号形成连续区间然后当做线段树做!每次查询时只需将结点变成对应的叶子结点区间在线段树上查询就可以了。
代码:
代码贴不上来。上链接好了。
http://vjudge.net/contest/viewSource.action?id=2726148
Problem D HDU 3698 Let the light guide us
题意:
n*m的两个矩阵,每个格子有两个值,一个是花费cost[i][j],一个是魔力值magic[i][j],(n<=100, m<=5000)要求每行选一个格子且格子对应的花费总和最小,任意响铃两行的格子魔力值满足条件|j-k|<=f[i, j]+f[i-1, k]。
分析:
CDQ分治做法还没想出来,之后在更新吧,看大家博客基本都是线段树做法..
dp[i][j]表示第i行选第j个格子的最小的花费。
分析一下, 对于任意相邻两行[i, j]和[i-1, k]的格子,[i-1, k]的花费dp[i-1][k]能够影响下一行k-magic[i-1, k]~k+magic[i-1, k]范围内格子的花费, [i, j]能够受上一行
j-magic[i,j]~j+magic[i, j]格子的花费的影响。这样用上一行花费dp[i-1][k]更新k-magic[i-1, k]~k+magic[i-1, k]最小值,到求dp[i, j]时, 查询j-magic[i, j]~j+magic[i,j]最小值min即可, dp[i][j] = min+cost[i][j] .
代码:
//Time 2014 09 01 , 10:22 #include <cstring> #include <algorithm> #include <cstdio> #include <iostream> #define in freopen("solve_in.txt", "r", stdin); #define bug(x) printf("Line %d : >>>>>>>\n", (x)); #define lson rt<<1, l, m #define rson rt<<1|1, m+1, r #define inf 0x0f0f0f0f #define pb push_back using namespace std; typedef long long LL; const int maxn = 5555; const int maxm = 111; int dp[maxn]; int n, m; int a[maxm][maxn], b[maxm][maxn], cover[maxn<<2], mi[maxn<<2]; void PushDown(int rt) { cover[rt<<1] = min(cover[rt<<1], cover[rt]); cover[rt<<1|1] = min(cover[rt<<1|1], cover[rt]); mi[rt<<1] = min(mi[rt<<1], cover[rt<<1]); mi[rt<<1|1] = min(mi[rt<<1|1], cover[rt<<1|1]); cover[rt] = inf; } void update(int rt, int l, int r, int L, int R, int c) { if(L <= l && R >= r) { cover[rt] = min(cover[rt], c); mi[rt] = min(mi[rt], cover[rt]); return; } int m = (l+r)>>1; PushDown(rt); if(L <= m) update(lson, L, R, c); if(R > m) update(rson, L, R, c); mi[rt] = min(mi[rt<<1], mi[rt<<1|1]); } int query(int rt, int l, int r, int L, int R) { if(L <= l && R >= r) { return mi[rt]; } int m = (l+r)>>1; int ans = inf; PushDown(rt); if(L<=m) ans = min(ans, query(lson, L, R)); if(R > m) ans = min(ans, query(rson, L, R)); return ans; } void build(int rt, int l, int r){ cover[rt] = mi[rt] = inf; if(l == r) { return; } int m = (l+r)>>1; build(lson); build(rson); } int main() { while(scanf("%d%d", &n, &m), n||m) { for(int i = 1; i <= n; i++) for(int j = 1; j <= m; j++) { scanf("%d", &a[i][j]); if(i == 1) dp[j] = a[i][j]; } for(int i = 1; i <= n; i++) for(int j = 1; j <= m; j++) { scanf("%d", &b[i][j]); } for(int i = 2; i <= n; i++) { build(1, 1, m); for(int j= 1; j <= m; j++) { int L = max(1, j-b[i-1][j]); int R = min(m, j+b[i-1][j]); update(1, 1, m, L, R, dp[j]); } for(int j = 1; j <= m; j++) { int L = max(1, j-b[i][j]); int R = min(m, j+b[i][j]); dp[j] = query(1, 1, m, L, R)+a[i][j]; } } cout<<*min_element(dp+1, dp+m+1)<<endl; } return 0; }
UPD:
问了一下CLQ终于知道CDQ是怎么搞的了。
分析一下,CDQ分治也是在转移的时候发挥作用,在需要求dp[i][j]的时候需要用上一行中dp[i-1][k]最小的来更新,且满足|j-k|<=magic[i][j]+magic[i-1][k]这个不等式, 按照CDQ分治的做法,需要将其变形为下面两个形式:
1. k > j 时, k-magic[i-1][k] <= magic[i][j]+j;
2. j > k 时, -magic[i-1][k]-k <= magic[i][j]-j;
分治的时候只需要分别考虑k > j时, dp[i-1][k]的最小值,及 j >k时, dp[i-1][k]的最小值, 然后更新就可以了。以k >j 为例,分治区间[l, r]表示相应格子的列的范围, 然后利用列坐标>=mid+1的dp[i-1][j]去更新列坐标
j <=m的dp[i][j], 具体可以这样先将i-1行每个格子按k-magic[i-1][k]增序排列,第i行格子按照magic[i][j]+j增序排列,有了单调性对于每个dp[i][j]只需要从队首往后扫, 并记录最小值, 可以证明这样对于magic[i][j]+j排在后面的dp[i][j]值的更新也是满足最优的,找到最优值就直接更新。
对于j > k的情况类似,这样一想中间过程又和前面几个题目类似了。
代码:
#include <iostream> #include <cstdio> #include <algorithm> #define pb push_back #define inf 0x0f0f0f0f #define bug(x) printf("line %d: >>>>>>>>>>>>>>>\n", (x)); #define in freopen("solve_in.txt", "r", stdin); #define SZ(x) ((int)x.size()) using namespace std; typedef pair<int, int> PII; const int maxn = 5555; int dp[111][maxn], magic[maxn][maxn], f[maxn][maxn]; PII t[4][maxn], t1[maxn]; void solve(int pos, int l, int r) { if(l == r) { t[0][l] = PII(l-magic[pos-1][l], l); t[1][l] = PII(magic[pos][l]+l, l); t[2][l] = PII(-l-magic[pos-1][l], l); t[3][l] = PII(magic[pos][l]-l, l); return ; } int mid = (l+r)>>1; solve(pos, l, mid); solve(pos, mid+1, r); int res = inf; for(int i = l, j = mid+1; i <= mid; i++) { while(j <= r && t[0][j].first <= t[1][i].first) { res = min(res, dp[pos-1][t[0][j].second]); j++; } dp[pos][t[1][i].second] = min(dp[pos][t[1][i].second], res+f[pos][t[1][i].second]); } res = inf; for(int i = mid+1, j = l; i <= r; i++) { while(j <= mid && t[2][j].first <= t[3][i].first) { res = min(res, dp[pos-1][t[2][j].second]); j++; } dp[pos][t[3][i].second] = min(dp[pos][t[3][i].second], res+f[pos][t[3][i].second]); } for(int k = 0; k < 4; k++) { int l1 = l, l2 = mid+1; for(int i = l; i <= r; i++) if(l1 <= mid && (l2 > r || t[k][l1].first < t[k][l2].first)) t1[i] = t[k][l1++]; else t1[i] = t[k][l2++]; for(int i = l; i <= r; i++) t[k][i] = t1[i]; } } int main() { int n, m; while(scanf("%d%d", &n, &m), n||m) { for(int i = 1; i <= n; i++) for(int j = 1; j <= m; j++) scanf("%d", &f[i][j]); for(int i = 1; i <= n; i++) for(int j = 1; j <= m; j++) scanf("%d", &magic[i][j]); for(int i = 1; i <= n; i++) { for(int j = 1; j <= m; j++) if(i == 1) dp[i][j] = f[i][j]; else { dp[i][j] = dp[i-1][j]+f[i][j]; } if(i != 1) solve(i, 1, m); } cout<<*min_element(dp[n]+1, dp[n]+m+1)<<endl; } return 0; }