1. 程式人生 > 其它 >前k大子段和問題(可持久化左偏樹)

前k大子段和問題(可持久化左偏樹)

今天午睡的時候忽然想到這樣一類問題:如何求一個序列(可正可負可零)的前k大(小)個子區間的和?隨即想到了一個$O(n(logn+logk))$的演算法,思路並不難

看到區間和,首先想到的就是構造出字首和,一般這麼搞準沒錯~

然後看到前k大問題,一般能想到的做法是構造出一個邊權非負的有向圖(顯式或隱式均可),轉換成k短路問題,用優先佇列求解

於是就自然而然地想到以每個左端點j作為起點,對其所有的右端點按權值構造構造一個小根堆,以點權之差作為邊權,然後建個超級源點作為起點,向每個小根堆連一條權值為對應左端點點權的邊(邊權可能為負,但是是從起點連出去的所以沒關係),假設序列長度為n,那麼問題的答案就是這n個小根堆構成的圖的前k短路

關鍵就在於如何快速構造出這個n個小根堆。注意到每個左端點j和j+1之間只差了一個結點,因此可以用類似主席樹的方法構造出可持久化的堆,可以用單次合併複雜度為$O(logn)$的左偏樹實現

 1 #include<bits/stdc++.h>
 2 #define l(u) ch[u][0]
 3 #define r(u) ch[u][1]
 4 using namespace std;
 5 const int N=1e5+10,M=N*40;
 6 int val[M],ds[M],ch[M][2],rt[N],tot,a[N],s[N],n,k;
 7 int newnode(int
x) {int u=++tot; ds[u]=l(u)=r(u)=0,val[u]=x; return u;} 8 int cpy(int u) {int w=++tot; ds[w]=ds[u],l(w)=l(u),r(w)=r(u),val[w]=val[u]; return w;} 9 void mg(int& w,int u,int v) { 10 if(!u||!v) {w=u|v; return;} 11 if(val[v]<val[u])swap(u,v); 12 w=cpy(u); 13 mg(r(w),r(u),v); 14 if
(ds[r(w)]>ds[l(w)])swap(l(w),r(w)); 15 ds[w]=ds[r(w)]+1; 16 } 17 struct D { 18 int u,g; 19 bool operator<(const D& b)const {return g>b.g;} 20 }; 21 priority_queue<D> q; 22 void solve(int k) { 23 bool flag=0; 24 while(q.size())q.pop(); 25 for(int i=0; i<n; ++i)q.push({rt[i+1],val[rt[i+1]]-s[i]}); 26 int cnt=0; 27 while(q.size()) { 28 int u=q.top().u,g=q.top().g; 29 q.pop(); 30 if(flag++)printf(" "); 31 printf("%d",g); 32 if(++cnt==k)break; 33 if(l(u))q.push({l(u),g-val[u]+val[l(u)]}); 34 if(r(u))q.push({r(u),g-val[u]+val[r(u)]}); 35 } 36 puts(""); 37 } 38 int main() { 39 scanf("%d%d",&n,&k); 40 for(int i=1; i<=n; ++i)scanf("%d",&a[i]); 41 for(int i=1; i<=n; ++i)s[i]=s[i-1]+a[i]; 42 for(int i=n; i>=1; --i)mg(rt[i],rt[i+1],newnode(s[i])); 43 solve(k); 44 return 0; 45 }

input:

10 10
1 3 -2 4 7 -5 -4 3 -3 5

output:

-9 -9 -6 -5 -4 -4 -4 -3 -2 -2