CQD(陳丹琦)分治 & 整體二分——專題小結
整體二分和CDQ分治
有一些問題很多時間都坑在斜率和凸殼上了麼……感覺斜率和凸殼各種搞不懂……
整體二分
整體二分的資料好像不是很多,我在網上找到了一篇不錯的資料:整體二分是個很神的東西,它可以把許多複雜的資料結構題化簡。它的精髓在於巧妙地利用了離線的特點,把所有的修改、詢問操作整體把握。
先說說第k大數吧,這種問題是整體二分的標誌性題目,什麼劃分樹啊,主席樹啊,樹套樹啊見了整體二分都得自嘆不如。首先對於一次詢問來說我們可以二分答案,然後通過驗證比答案大的數有多少個來不斷地縮小答案範圍直至得到一個準確的答案。而對於多個詢問我們同樣可以這麼做,只不過對每一個詢問我們都需要判定一下,以決定它被劃分到哪一個答案的區間裡。這個判定過程就是通過比較比二分的mid大的數的個數和k。同時我們看到,如果比二分的mid大的數的個數小於k了,我們是要去尋找小的答案,那麼這些比mid大的數在以後的遞迴裡始終會對答案有貢獻,所以我們沒必要去做重複的工作,只需要把這些數的個數累積到貢獻裡,以後遞迴的時候就不用考慮這些數了。具體地,我們把數列裡的數也和詢問一起遞迴,這樣這些數也會被分到屬於的答案區間裡,並且只對相應區間裡的詢問有影響。如果有修改呢,我們把修改看成一個數就好了,一樣可以隨著遞迴不斷地被劃分下去。
我們看到,整體二分的過程實質上是個按照數值來劃分操作序列的過程,於是我們的複雜度也就和操作序列的長度線性相關,那麼我們在中間維護一些資訊的時候,就一定不能有何數列長線性相關的東西,否則會破壞其時間複雜度,具體的複雜度證明請見2013年集訓隊XHR論文。
貼個典型性的程式碼吧,帶修改區間第k小數
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> #include<cstring> #define maxn 220000 #define inf 1000000000 using namespace std; struct query { int x,y,k,s,tp,cur; }q[maxn],q1[maxn],q2[maxn]; int a[maxn],ans[maxn],tmp[maxn],t[maxn]; int n,m,num,cnt; void add(int x,int y) { for (int i=x;i<=n;i+=(i&-i)) t[i]+=y; } int ask(int x) { int tmp=0; for (int i=x;i>0;i-=(i&-i)) tmp+=t[i]; return tmp; } void divide(int head,int tail,int l,int r) { //cout<<head<<' '<<tail<<' '<<l<<' '<<r<<endl; if (head>tail) return ; if (l==r) { for (int i=head;i<=tail;i++) if (q[i].tp==3) ans[q[i].s]=l;//,cout<<l<<endl; return ; } int mid=(l+r)>>1; for (int i=head;i<=tail;i++) { if (q[i].tp==1&&q[i].y<=mid) add(q[i].x,1); else if (q[i].tp==2&&q[i].y<=mid) add(q[i].x,-1); else if (q[i].tp==3) tmp[i]=ask(q[i].y)-ask(q[i].x-1); } for (int i=head;i<=tail;i++) { if (q[i].tp==1&&q[i].y<=mid) add(q[i].x,-1); else if (q[i].tp==2&&q[i].y<=mid) add(q[i].x,1); } int l1=0,l2=0; for (int i=head;i<=tail;i++) if (q[i].tp==3) { if (q[i].cur+tmp[i]>q[i].k-1)//q[i].cur+tmp[i]表示累積了多少個數 q1[++l1]=q[i]; else { q[i].cur+=tmp[i]; q2[++l2]=q[i]; } } else { if (q[i].y<=mid) q1[++l1]=q[i]; else q2[++l2]=q[i]; } for (int i=1;i<=l1;i++) q[head+i-1]=q1[i]; for (int i=1;i<=l2;i++) q[head+l1+i-1]=q2[i]; divide(head,head+l1-1,l,mid); divide(head+l1,tail,mid+1,r); } int main() { //freopen("ranking.in","r",stdin); //freopen("ranking.out","w",stdout); scanf("%d%d",&n,&m); for (int i=1;i<=n;i++) { scanf("%d",&a[i]); q[++num].x=i; q[num].y=a[i]; q[num].tp=1; q[num].s=0; } char sign; int x,y,z; for (int i=1;i<=m;i++) { scanf("\n%c",&sign); if (sign=='Q') { scanf("%d%d%d",&x,&y,&z); q[++num].x=x,q[num].y=y,q[num].k=z; q[num].tp=3; q[num].s=++cnt; } else { scanf("%d%d",&x,&y); q[++num].x=x; q[num].y=a[x]; q[num].tp=2; q[num].s=0; q[++num].x=x; q[num].y=y; q[num].tp=1; q[num].s=0; a[x]=y; } } divide(1,num,0,inf); for (int i=1;i<=cnt;i++) printf("%d\n",ans[i]); return 0; }
CQD分治
來看一道例題:
[Poi2011] Meteors
有n個國家和m個空間站,每個空間站都屬於一個國家,一個國家可以有多個空間站,所有空間站按照順序形成一個環,也就是說,m號空間站和1號空間站相鄰。
現在,將會有k場流星雨降臨,每一場流星雨都會給區間[li,ri]內的每個空間站帶來ai單位的隕石,每個國家都有一個收集隕石的目標pi,即第i個國家需要收集pi單位的隕石。
詢問:每個國家最早完成隕石收集目標是在第幾場流星雨過後。
資料範圍:1<=n,m,k<=300000
對於單個查詢(假設為第i個國家),我們可以二分k,每次對於一個區間[l,r],手動模擬一下在第mid場流星雨過後,第i個國家一共收集到了多少單位的隕石,如果比pi大,那麼答案在[l,mid]範圍內,否則答案在[mid+1,r]範圍內。
對於多組查詢,我們也可以這麼做。首先,我們需要用一個列表id[]記錄所有查詢的編號,剛開始的時候,id[]自然是遞增的.同時,我們用一個數組cur[i]記錄下,第i個國家在l-1場流星雨過後,收集到的隕石的數目。
主過程為void solve(int head,int tail,int l,int r),表示對於id[head]到id[tail]的所有詢問,在[l,r]範圍內查詢答案,通過上一層的操作,我們保證id[head]到id[tail]的所有詢問的答案都在[l,r]範圍內。
首先,我們先模擬[l,mid]這麼多次操作(在詢問重新劃分之後,必須要再次模擬,將陣列清空),用樹狀陣列或者是線段樹計算出在[l,mid]場流星雨之後,每個空間站收集到的隕石的數目。
然後我們查詢,每個國家收集到的隕石的數目,要注意的是,我們需要用連結串列儲存每個國家對應的空間站,並且一一列舉,用tmp[id[i]]表示國家id[i]收集到的隕石的數目。
那麼從[1,mid]這麼多次操作之後,國家id[i]收集到的隕石數目就是tmp[id[i]]+cur[id[i]],如果tmp[id[i]]+cur[id[i]]>p[id[i]],那麼表明對於國家id[i],其答案在[l,mid]這個範圍內,否則其答案在[mid+1,r]範圍內,並將tmp[id[i]]累加到cur[id[i]]上。
還有一個坑點是,tmp[id[i]]可能很大,會爆掉long long,所以如果列舉一個國家的所有空間站的時候,發現tmp[id[i]]已經大於p[id[i]]了,那麼就break好了,不然會出錯。
因為可能會出現怎麼也無法滿足的情況,所以我們需要多增加一場流星雨,這場流星雨的數量為infi,保證能夠讓所有國家都滿足要求,那麼最後,對於所有答案為k+1的詢問,輸出NIE就行了。
總的來說,整體二分就是將所有詢問一起二分,然後獲得每個詢問的答案。
CDQ相比整體二分略有不同,整體二分是對答案進行二分,而CDQ分治則是對於所有操作進行二分。
一開始的時候,一般都需要先排序一下,將所有的操作按照一定的性質排起來(Cash一題是按-a[i]/b[i]從大到小排序,Mokia一題是按照x座標從小到大排序),否則的話和直接暴力沒有區別。
接著就是對所有操作進行二分,對於一個區間[l,r],我們首先要對[l,mid]範圍內的操作進行查詢,所以需要將[l,r]範圍內的操作重排,保證[l,mid]範圍內的操作的id都<=mid,然後再對[l,mid]範圍內的操作進行遞迴求解。
遞迴完之後,[l,mid]範圍內的所有操作都已經按照先後的順序排序好了,但是[mid+1,r]範圍內的操作還是按照某個關鍵字排序。我們根據[l,mid]範圍內的操作更新[mid+1,r]範圍內的操作(在Cash一題中是更新它們的f[]陣列,在Mokia中則要和整體二分差不多,先用[l,mid]範圍內的操作模擬,再更新[mid+1,r]範圍內所有查詢的答案)。
更新完之後,我們再遞迴求解[mid+1,r]。
因為大神CDQ的論文已經講得很詳細了,網上關於CDQ分治的資料也挺多的,就不再講了,感覺這些神奇的演算法還是挺好用的,尤其是在自己懶的時候(許多高階資料結構題的程式碼量真是醉了……)。
三份程式碼見下面:
http://victorwonder.is-programmer.com/posts/70210.html?utm_source=tuicool
Meteors
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <utility>
#include <bitset>
#include <vector>
#include <string>
#include <stack>
#include <queue>
#include <ctime>
#include <cmath>
#include <list>
#include <map>
#include <set>
using namespace std;
typedef long long ll;
typedef double D;
typedef pair<int,int> pr;
const int infi=1000000010;
const int N=500010;
const int M=2000100;
struct node{int x,y,z;}p[N];
int n,m,k,c[N],id[N],ans[N];
int g[N],to[N],nxt[N],tot;
int tol[N],tor[N];
ll h[N],tmp[N],cur[N];
void read(int &a) {
char ch; while (!((ch=getchar())>='0'&&ch<='9'));
a=ch-'0'; while ((ch=getchar())>='0'&&ch<='9') (a*=10)+=ch-'0';
}
void add(int pos,int x) {while (pos<=m) h[pos]+=x,pos+=(pos&-pos);}
void adddt(int x,int y,int z) {add(x,z); add(y+1,-z);}
ll sum(int pos) {ll t=0; while (pos>0) t+=h[pos],pos-=(pos&-pos);return t;}
void addop(int x,int y,int z,int i) {p[i].x=x; p[i].y=y; p[i].z=z;}
void addpt(int x,int y) {to[++tot]=y; nxt[tot]=g[x]; g[x]=tot;}
void solve(int head,int tail,int l,int r) {
if (head>tail) return;
int i,k,x,mid=(l+r)>>1,lnum=0,rnum=0;
if (l==r)
{
for (i=head;i<=tail;i++) ans[id[i]]=l;
return;
}
for (i=l;i<=mid;i++)
{
if (p[i].x<=p[i].y) adddt(p[i].x,p[i].y,p[i].z);
else adddt(p[i].x,m,p[i].z),adddt(1,p[i].y,p[i].z);
}
for (i=head;i<=tail;i++)
{
tmp[id[i]]=0;
for (k=g[id[i]];k;k=nxt[k])
{
tmp[id[i]]+=sum(to[k]);
if (tmp[id[i]]+cur[id[i]]>c[id[i]]) break;
}
if (cur[id[i]]+tmp[id[i]]>=c[id[i]]) tol[++lnum]=id[i];
else tor[++rnum]=id[i],cur[id[i]]+=tmp[id[i]];
}
for (i=l;i<=mid;i++)
{
if (p[i].x<=p[i].y) adddt(p[i].x,p[i].y,-p[i].z);
else adddt(p[i].x,m,-p[i].z),adddt(1,p[i].y,-p[i].z);
}
for (i=0;i<lnum;i++) id[head+i]=tol[i+1];
for (i=0;i<rnum;i++) id[head+lnum+i]=tor[i+1];
solve(head,head+lnum-1,l,mid);
solve(head+lnum,tail,mid+1,r);
}
int main() {
int i,x,y,z;
scanf("%d%d",&n,&m);
for (i=1;i<=m;i++) {
scanf("%d",&x);
addpt(x,i);
}
for (i=1;i<=n;i++)
{
scanf("%d",&c[i]);
id[i]=i;
}
scanf("%d",&k);
for (i=1;i<=k;i++)
{
scanf("%d%d%d",&x,&y,&z);
addop(x,y,z,i);
}
addop(1,m,infi,++k);
solve(1,n,1,k);
for (i=1;i<=n;i++)
if (ans[i]!=k) printf("%d\n",ans[i]);
else
puts("NIE");
return 0;
}
Cash
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <utility>
#include <bitset>
#include <vector>
#include <string>
#include <stack>
#include <queue>
#include <ctime>
#include <cmath>
#include <list>
#include <map>
#include <set>
using namespace std;
typedef long long ll;
typedef double D;
typedef pair<int,int> pr;
const int infi=2147483647;
const int N=100010;
struct node{D a,b,r,k;}p[N];
struct func{D x,y;}f[N],v[N];
D ans[N],S;
int n,id[N],s[N];
bool operator < (func a,func b) {return a.x<b.x||a.x==b.x&&a.y<b.y;}
bool cmp(const int &a,const int &b) {return p[a].k>p[b].k;}
D cross(func a,func b,func c) {return (b.x-a.x)*(c.y-b.y)-(b.y-a.y)*(c.x-b.x);}
D calc(func t,int i) {return t.x*p[i].a+t.y*p[i].b;}
void solve(int l,int r,D maxnum) {
if (l==r)
{
ans[l]=max(ans[l],maxnum);
f[l].y=ans[l]/(p[l].a*p[l].r+p[l].b);
f[l].x=f[l].y*p[l].r;
return;
}
int i,mid=(l+r)>>1,t1=l,t2=mid+1,t=0,h=0;
for (i=l;i<=r;i++) if (id[i]<=mid) s[t1++]=id[i];
else s[t2++]=id[i];
memcpy(id+l,s+l,sizeof(int)*(r-l+1));
solve(l,mid,maxnum);
for (i=l;i<=mid;v[t++]=f[i++]) while (t&&cross(v[t-2],v[t-1],f[i])>=0) t--;
for (i=mid+1;i<=r;i++)
{
while (h<t-1&&calc(v[h],id[i])<calc(v[h+1],id[i])) h++;
ans[id[i]]=max(ans[id[i]],calc(v[h],id[i]));
}
solve(mid+1,r,ans[mid]);
merge(f+l,f+mid+1,f+mid+1,f+r+1,v);
memcpy(f+l,v,sizeof(func)*(r-l+1));
}
int main()
{
int i;
scanf("%d%lf",&n,&S);
for (i=0;i<n;i++)
{
scanf("%lf%lf%lf",&p[i].a,&p[i].b,&p[i].r);
p[i].k=-p[i].a/p[i].b;
id[i]=i;
}
sort(id,id+n,cmp);
solve(0,n-1,S);
printf("%.3lf",ans[n-1]);
return 0;
}
Mokia
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <utility>
#include <bitset>
#include <vector>
#include <string>
#include <stack>
#include <queue>
#include <ctime>
#include <cmath>
#include <list>
#include <map>
#include <set>
using namespace std;
typedef long long ll;
typedef double D;
typedef pair<int,int> pr;
const int infi=2147483647;
const int N=500010;
const int M=2000100;
struct node{int op,x,y,z,t,id;}p[N],s[N];
int S,n,num,tot,id[N];
int h[M],ans[N];
bool cmp(const node &a,const node &b) {return a.x<b.x;}
void addquery(int i,int op,int x,int y,int z,int id) {
p[i].id=i; p[i].op=op; p[i].x=x; p[i].y=y; p[i].z=z; p[i].t=id;
}
void add(int pos,int x) {while (pos<=n) h[pos]+=x,pos+=(pos&(-pos));}
ll sum(int pos) {ll t=0; while (pos>0) t+=h[pos],pos-=(pos&(-pos));return t;}
void solve(int l,int r) {
if (l==r) return;
int mid=(l+r)>>1,i,t1=l-1,t2=mid,t=l;
for (i=l;i<=r;i++) if (p[i].id<=mid) s[++t1]=p[i];
else s[++t2]=p[i];
memcpy(p+l,s+l,sizeof(node)*(r-l+1));
for (i=mid+1;i<=r;i++) if (p[i].op==2)
{
for (;t<=mid&&p[t].x<=p[i].x;t++) if (p[t].op==1) add(p[t].y,p[t].z);
ans[p[i].t]+=sum(p[i].y)*p[i].z;
}
for (i=l;i<t;i++) if (p[i].op==1) add(p[i].y,-p[i].z);
solve(l,mid); solve(mid+1,r);
}
int main() {
int i,k,x,y,z,w;
scanf("%d%d",&S,&n);
while (~scanf("%d",&k))
{
if (k==3) break;
if (k==1)
{
scanf("%d%d%d",&x,&y,&z);
addquery(++tot,1,x,y,z,0);
} else
{
scanf("%d%d%d%d",&x,&y,&z,&w); num++;
addquery(++tot,2,z,w,1,num);
addquery(++tot,2,x-1,y-1,1,num);
addquery(++tot,2,x-1,w,-1,num);
addquery(++tot,2,z,y-1,-1,num);
}
}
sort(p+1,p+1+tot,cmp);
solve(1,tot);
for (i=1;i<=num;i++) printf("%d\n",ans[i]);
return 0;
}
CDQ分治理解
cdq分治是一種特別的分治方法,它由cdq神牛於09國家集訓隊作業中首次提出,因此得名。
首先,cdq分治屬於分治的一種。它一般只能處理非強制線上的問題,除此之外這個演算法作為某些複雜演算法的替代品幾乎是沒有缺點的。
考慮這樣一個問題,有若干詢問和若干修改,要求在O(nlogn)時間複雜度內回答所有的詢問。(下把詢問與修改統稱為操作)
我們把[l,r]代表當前處理的操作的區間,即處理第l個到第r個操作,先找到區間的中間m=(l+r)/2
(1)然後對於前一半[l,m]我們先遞迴解決。
(2)對於所有在[l,m]內的修改操作,列舉處理它對於[m,r]內的所有操作『影響』。
(3)之後遞迴處理[m,r]這一區間。
複雜度分析:分治共有log(n)層,那麼要求每一層都線上性的時間複雜度內完成,才能保證總時間複雜度是O(nlogn)
對於不同的題目,修改和詢問是不一樣的。在某些修改之間互相獨立的題目下,還可以調換(2)(3)的順序。
當然cdq分治也可以想其他的分治方法一樣,巧妙利用歸併排序,來實現每層O(logn)
就拿斜率優化dp來說,並且假設要用BST維護決策下凸性,那麼就不得不寫一個BST。想在考場上調出一個這樣的程式,不說很難,也是很耗時耗力的。
如果運用cdq分治,事情就簡單得多了:(1)(3)步不說,在(2)中將[l,m]所有的已經統計完的決策點排序,然後可以很方便地獲得這個下凸殼,直接兩個指標掃一下就可以在O(n)時間複雜度裡統計出他們對[m,r]內的狀態的影響了。這樣做程式碼很短,容易在考試時候調試出來,節省時間。
好吧,我還是很不擅長描述演算法...
沒看懂的可以到cdq分治相關看下這篇,寫得很不錯
例題:
T1:BZOJ 3110: [Zjoi2013]K大數查詢
網上看到各種Trie套Trie做法,顯然是在坑,因為這題理論分析發現Trie套Trie無論在時間複雜度還是空間複雜度都是掛的,至於最後資料出的弱是另一回事。這題的正解應該是cdq分治。把答案當軸進行分治,對答案的操作,統計出當答案為m=(l+r)時候,每個詢問的區間裡>=它的數有多少個,這個用從頭到尾掃一遍,然後線段樹維護的方法做。
如果詢問i區間[li,ri]裡>=m的數<ci個那麼顯然,它的ans應該<m,歸入0類;反之,則應該大於等於m,歸入1類。
如果修改i,增加的數的值>=m那麼歸入1類,否則歸入0類。然後我們發現,歸入0類的詢問i,在之後的分治中被檢驗到的ans必然是<=m的,那麼對於每個被歸入1類的修改,它所有的在區間[l[i],r[i]]內的數都會被統計進入『>=驗證到的ans』的數的個數中,那麼直接在ci上減去就可以了。
總時間複雜度O(nlog^2n)
cdq分治:http://ideone.com/QmgPc6
Trie套Trie:http://ideone.com/v1VeSN
T2:BZOJ 1492: [NOI2007]貨幣兌換Cash
斜率優化dp,相當強大的應用。這個題不同於一般的斜率優化dp,需要用bst維護凸殼。用了cdq分治之後,詢問和決策就都變成離線了,直接排個序,當成普通的斜率優化dp來做,相當犀利。這裡有一個特殊情況,就是對[m+1,r]也是要進行某個關鍵字的排序的,所以必須事先排序好並記錄。
注意,這個時候時間複雜度是O(nlogn) 空間複雜度是O(nlogn)的,如果題目卡記憶體就不要用了,不過也沒有這麼sxbk的出題人吧。
http://ideone.com/ISBnao
T3:BZOJ 2527: [Poi2011]Meteors
這題不得不說有點神。大體思路和T1一樣,然後就是這題最神的地方,必須要開unsigned long long才能通過本題 ....
如果程式調不出錯,或許可以試試修改掉這個玩意兒。我的程式執行速度極慢= =
http://ideone.com/M99tLh
T4: Codeforces #232 div.1 C - On Changing Tree
昨天考試遇到的題目。當時寫了樹鏈剖分,然後爆棧了呵呵,沒心情寫手工棧導致爆零。今天突然意識到cdq分治可做,複雜度O(nlogn),觀察了一下時限3s,於是各種亂搞水過
http://ideone.com/KdMvZ0
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
程式碼:
/*Time 2014 08 31 , 19:26
*/
#include <bits/stdc++.h>
#define in freopen("solve_in.txt", "r", stdin);
#define bug(x) printf("Line %d : >>>>>>>\n", (x));
using namespace std;
typedef long long LL;
const int maxn = 50000 + 100;
LL x1[maxn][2], x2[maxn][2], ans[maxn];
int cnt;
struct Node
{
int l, r, type;
LL c;
int id;
} q[maxn];
int rk[maxn], t1[maxn], t2[maxn];
int n, m;
LL query(LL a[][2], int x)
{
LL res = 0;
for(; x > 0; x -= (x&(-x)))
{
if(a[x][0] == cnt) res += a[x][1];
}
return res;
}
LL query(int l, int r)
{
return query(x1, l)*(r-l+1)+ (r+1)*(query(x1, r)-query(x1, l)) - (query(x2, r)-query(x2, l));
}
void add(LL a[][2], int x, LL c)
{
for(; x <= n; x += ((-x)&x))
{
if(a[x][0] == cnt) a[x][1] += c;
else a[x][0] = cnt, a[x][1] = c;
}
}
void add(int l, int r, int c)
{
add(x1, l, c);
add(x2, l, (LL)l*c);
add(x1, r+1, -c);
add(x2, r+1, (LL)(r+1)*(-c));
}
void solve(int ll, int rr, int l, int r)
{
if(l > r) return;
if(ll == rr)
{
for(int i = l; i <= r; i++)
if(q[rk[i]].type == 2)
{
ans[rk[i]] = ll;
}
return;
}
int m1 = (ll+rr)>>1, m2 = (l+r)>>1;
cnt++;
t1[0] = t2[0] = 0;
for(int i = l; i <= r; i++)
{
if(q[rk[i]].type == 1)
{
if(q[rk[i]].c <= m1)
{
add(q[rk[i]].l, q[rk[i]].r, 1);
t1[++t1[0]] = rk[i];
}
else
{
t2[++t2[0]] = rk[i];
}
}
else
{
LL xx = query(q[rk[i]].l, q[rk[i]].r);
if(xx < (LL)q[rk[i]].c)
{
q[rk[i]].c -= xx;
t2[++t2[0]] = rk[i];
}
else
{
t1[++t1[0]] = rk[i];
}
}
}
m2 = l+t1[0]-1;
for(int i = l; i <= r; i++)
{
if(i <= m2)
{
rk[i] = t1[i-l+1];
}
else
{
rk[i] = t2[i-m2];
}
}
solve(ll, m1, l, m2);
solve(m1+1, rr, m2+1, r);
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= m; i++)
{
rk[i] = i;
scanf("%d%d%d%lld", &q[i].type, &q[i].l, &q[i].r, &q[i].c);
if(q[i].type == 1) q[i].c = (LL)n-q[i].c+1;
q[i].id = i;
}
solve(1, 2*n+1, 1, m);
for(int i = 1; i <= m; i++)
{
if(q[i].type == 2)
{
printf("%d\n", n-ans[i]+1);
}
}
return 0;
}
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;
}
UPDATE:問了一下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;
}