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; }
