線段樹入門
-
瞭解線段樹
線段樹是一種二叉搜索樹,它將一個區間劃分成一些較小的區間,最終劃分成單元區間,每個單元區間對應線段樹中的一個葉結點,表示線段上一個點。如下圖。
-
線段樹的優勢
首先,先知道在線段樹上進行的操作複雜度是 O(log2n)。
現在,用最樸樸素的方式,在一個一維數組 a[n] 中,我們要修改某一個點的值時,比如將原數組中 a[3]=3 改成 a[3]=6 ,只需要 a[3]=6; 即可,複雜度爲O(1);若需要求 a[k] (0≤i≤k<j<n) 的和時,需就需要遍歷數組,複雜度爲O(n)。
雖然從單次操作看來線段樹沒有明顯的優勢,但是,當我們需要進行多次修改和查詢操作時,再使用樸素方法時就是 O(n²) 的複雜度,由於操作數次數很多,這種方法在競賽中是完全不能接受的。 而使用線段樹時,就是 O(nlog2n) 的複雜度。所以,再多次查詢和多次修改的操作中,線段樹是一個很有優勢的數據結構。
通過線段樹,就可以在 O(log2n) 的時間複雜度下完成區間查詢、區間修改、單點查詢、單點修改、區間最值等操作。
接下來以區間求和介紹線段樹的思路和寫法。 -
建樹
#include<bits/stdc++.h> #define lson l, mid, rt<<1 //左子樹 #define rson mid+1, r, rt<<1|1 //右子樹 using namespace std; const int MAX = 100; //數據量 int sum[MAX<<2]; //採用順序存儲結構建樹,注意建樹所用空間是4*MAX void push_up(int rt){ //向上更新 sum[rt] = sum[rt<<1] + sum[rt<<1|1]; //根結點的值等於左右子樹區間的值的和 } void build(int l, int r, int rt){ //l,r是區間範圍,rt是根結點下標 if(l==r){ //當l==r時,就是葉節點了,相當於線段上的某一點 sum[rt] = 1; //初始化,根據需要決定 return ; } int mid = (l+r)>>1; build(lson); //遞歸創建左子樹 build(rson); //遞歸創建右子樹 push_up(rt); }
-
單點查詢
//單點查詢 int query(int x, int l, int r, int rt){ //x是要查詢的點,l,r是線段區間,rt是樹的根節點 if(l==r && l==x){ //達到葉結點,並且與要查詢的點吻合 return sum[rt]; } int mid = (l+r)>>1; if(x<=mid) query(x, lson); //查詢的點在左區間 else query(x, rson); //查詢的點在右區間 }
-
區間查詢
//區間查詢 int query(int b, int e, int l, int r, int rt){ //b,e是求和區間,l,r是線段區間,rt是樹的根節點 if(b<=l && e>=r){ //當前區間完全包含在所求區間內 return sum[rt]; //返回該結點的值,就是該結點覆蓋線段上區間[l,r]的點的值的和 } int mid = (l+r)>>1, ans = 0; /* 這裏理解一下,求區間和的時候分成三種情況,所求區間: 1.完全包含在左子樹包含的區間[l, mid]內; 2.完全包含在右子樹包含的區間[mid+1, r]內; 3.在左子樹區間有一部分,右子樹的區間有一部分,就要將所求區間分成兩部分求和 */ if(l<=mid) ans += query(b, e, lson); if(e>=mid+1) ans += query(b, e, rson); return ans; }
//區間查詢第二種容易理解的方式 int query(int b, int e, int l, int r, int rt){ //b,e是求和區間,l,r是線段區間,rt是樹的根節點 if(b==l && e==r){ //當前區間與所求區間吻合 return sum[rt]; //返回該結點的值,就是該結點覆蓋線段上區間[l,r]的點的值的和 } int mid = (l+r)>>1, ans = 0; //這裏依舊分成三種情況 if(e<=mid) ans += query(b, e, lson); //完全包含在左子樹包含的區間[l, mid]內 else if(l>=mid+1) ans += query(b, e, rson); //完全包含在右子樹包含的區間[mid+1, r]內 else ans = query(b, mid, lson) + query(mid+1, e, rson); //在左子樹區間有一部分,右子樹的區間有一部分 return ans; }
-
單點更新
//單點更新 void update(int x, int l, int r, int rt){ //x需要更新的點的位置 if(l==r && l==x){ sum[rt] = newdata; //更新值 return ; } int mid = (l+r)>>1; if(x<=mid) update(x, lson); //需要更新的點在左子樹 else update(x, rson); //需要更新的點在右子樹 }
-
區間更新
對於更新區間[l, r]內的值(區間內每一個點同時加上某一個數),如果使用普通的數組,只需要遍歷一邊即可,時間複雜度是 O(n) ,同樣,如果使用線段樹的結構,對區間內每個點的值進行更新時,需要對每個數(O(n))先進行查找(O(log2n))再更改,時間複雜度爲O(nlog2n),反而高於普通方法,所以在線段樹上進行區間更新時,並不是採用直接更新某些點的方式,而是採用更新區間並標記,在需要時進行標記下放的方式完成的。稱爲lazy-tag方法。
lazy-tag方法當修改的是一整個區間時,只對這個線段區間進行整體上的修改,其內部每個元素的內容先不做修改,只有當這部分線段的一致性被破壞時,才把變化的值傳遞給子區間。那麼每次區間修改的時間複雜度就是O(log2n),N次區間修改操作的時間複雜度就是O(nlog2n)。
下面用圖解的形式來說明。與前面保持一致,使用 sum[MAX] 存放樹,新增一個 add[MAX] 用來保存相應節點的 lazy 值。
初始狀態:
首先將區間 [1, 3] 加上 1
a.[1,3] 分成 [1, 2] 和 [3,3]兩個區間
b.更新兩個區間的和以及對應的 lazy 值 再將區間 [2,4] 加 1
a.區間 [2,4] 分爲 [2,2] 和 [3,4] 兩個區間。
b.更新 [2, 2] 時,經過 [1,2] ,[1,2] 區間的 lazy 值不爲零,說明該區間的點更新過,需要先將 lazy 標記下放到兩個子區間,然後標記清零。再繼續深入。
c.[3,4] 恰好爲需要修改的區間,直接更新區間和,並將 lazy 標記。
//標記下放 void push_down(int rt, int n){ //n是區間長度 add[rt<<1] += add[rt]; //更新子區間lazy add[rt<<1|1] += add[rt]; sum[rt<<1] += (n-(n>>1))*add[rt]; //更新子區間 sum[rt<<1] += (n>>1)*add[rt]; add[rt] = 0; //取消本層lazy } //區間更新 void update(int b, int e, int x, int l, int r, int rt){ if(b<=l && e>=r){ add[rt] += x; //lazy增加 sum[rt] += (r-l+1)*x; //區間和增加 return ; } push_down(rt, (r-l+1)); //lazy下放 int mid = (l+r)>>1; if(b<=mid) update(b, e, x, lson); //更新左區間 if(e>=mid+1) update(b, e, x, rson); //更新右區間 push_up(rt); }
-
離散化
當節點規模很大時,由於空間限制,很難在程序中建立二叉樹時,就需要用到離散化,離散化實際上就是將大的二叉樹壓縮成較小的二叉樹,但是壓縮前後子區間的關係不變。
例如一塊宣傳欄,橫向長度刻度爲1→10,貼四張不同顏色的海報,他們和宣傳欄等寬,長度分別爲[1,3] 、[2,5]、[3,8]、[3,10] ,並且用後者覆蓋前者,問最後能見幾種顏色的海報。
離散化步驟如下:
(1)提取四張海報的八個頂點 1, 3, 2, 5, 3, 8, 3, 10
(2)排序並刪除相同的端點 1, 2, 3, 5, 8, 10
(3)把原線段上的端點映射到新線段上:
新的 4 張海報爲[1,3]、 [2,4]、 [3,5]、 [3,6] ,覆蓋關係不變,新的宣傳欄長度壓縮到 6 。
練習
Lost Cows
A Simple Problem with Integers
敵兵佈陣
Minimum Inversion Number
Just a Hook
I Hate It
Billboard
Ultra-QuickSort
Buy Tickets
Stars
Who Gets the Most Candies?
-
題意:給出2->n頭牛前面有多少頭比他編號少的數目,求出每頭牛原來的編號,
-
思路:從後面向前遍歷,對第m頭牛前面有a頭牛,表示此牛在剩餘的牛中排名a+1.用線段樹維護區間裏面未知編號的牛的數量,找到滿足條件的區間,區間右端點即爲此牛的編號。找到後將此牛標記一下已知編號。
-
代碼
#include<iostream> #include<stdio.h> using namespace std; int sum[17000]; int ans[8005], que[8005]; int q, ran; void build(int n, int l, int r){ if(l==r){ sum[n] = 1; return ; } int m = (l+r)>>1; build(2*n, l, m); build(2*n+1, m+1, r); sum[n] = sum[2*n] + sum[2*n+1]; } int query(int n, int l, int r){ if(l==r) return r; int m = (l+r)>>1; if(ran<=sum[2*n]){ return query(2*n, l, m); } else{ ran -= sum[2*n]; return query(2*n+1, m+1, r); } } void update(int n, int l, int r){ if(l==r){ sum[n]--; return ; } int m = (l+r)>>1; if(ran>m) update(2*n+1, m+1, r); else update(2*n, l, m); sum[n] = sum[2*n] + sum[2*n+1]; } int main(){ scanf("%d", &q); for(int i=1; i<q; i++) scanf("%d", &que[i]); que[0] = 0; build(1, 1, q); for(int i=q-1; i>=0; i--){ ran = que[i]+1; ans[i] = query(1, 1, q); ran = ans[i]; update(1, 1, q); } for(int i=0; i<q; i++) printf("%d\n", ans[i]); return 0; }
-
模板題,就是區間查詢和區間修改。區間修改時,直接修改點必然會超時,這就用到了區間修改的 lazy 標記。還有就是注意一下數據範圍,使用 long long 。
-
代碼
#include<iostream> #include<stdio.h> #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 #define ll long long using namespace std; const int MAXN = 1e5+10; ll sum[MAXN<<2]={0}, add[MAXN<<2]={0}; void push_up(int rt){ sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } void push_down(int rt, int m){ if(add[rt]){ add[rt<<1] += add[rt]; add[rt<<1|1] += add[rt]; sum[rt<<1] += (m-(m>>1))*add[rt]; sum[rt<<1|1] += (m>>1)*add[rt]; add[rt] = 0; } } void build(int l, int r, int rt){ add[rt] = 0; if(l == r){ scanf("%lld", &sum[rt]); return ; } int mid = (l+r)>>1; build(lson); build(rson); push_up(rt); } void update(int a, int b, ll c, int l, int r, int rt){ if(a<=l && b>=r){ sum[rt] += (r-l+1)*c; add[rt] += c; return ; } push_down(rt, r-l+1); int mid = (l+r)>>1; if(a<=mid) update(a, b, c, lson); if(b>mid) update(a, b, c, rson); push_up(rt); } ll query(int a, int b, int l, int r, int rt){ if(a<=l && b>=r) return sum[rt]; push_down(rt, r-l+1); int mid = (l+r)>>1; ll ans = 0; if(a<=mid) ans += query(a, b, lson); if(b>mid) ans += query(a, b, rson); return ans; } int main(){ int n, m; scanf("%d%d", &n, &m); build(1, n, 1); while(m--){ char str[2]; int a, b; ll c; scanf("%s", str); if(str[0] == 'C'){ scanf("%d%d%lld", &a, &b, &c); update(a, b, c, 1, n, 1); } else{ scanf("%d%d", &a, &b); printf("%lld\n", query(a, b, 1, n, 1)); } } return 0; }
-
思路:單點更新和區間求和。模板題
-
代碼
#include<iostream> #include<stdio.h> using namespace std; #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 const int MAXN = 50005; int sum[MAXN<<2]={0}; //建樹 void build(int l, int r, int rt){ if(l == r){ scanf("%d", &sum[rt]); return ; } int mid = (l+r)>>1; build(lson); build(rson); sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } //單點修改 void update(int i, int j, int l, int r, int rt){ if(l==r){ sum[rt] += j; return ; } int mid = (l+r)>>1; if(i<=mid) update(i, j, lson); else update(i, j, rson); sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } //區間查詢 int query(int i, int j, int l, int r, int rt){ if(i==l && j==r) return sum[rt]; int mid = (l+r)>>1; if(j>=mid+1 && i<=mid) return query(i, mid, lson)+query(mid+1, j, rson); else if(j<=mid) return query(i, j, lson); else /*(i>=mid+1)*/ return query(i, j, rson); } int main(){ int m, n, i, j; string str; scanf("%d", &m); for(int k=1; k<=m; k++){ printf("Case %d:\n", k); scanf("%d", &n); build(1, n, 1); while(1){ cin>> str; if(str=="End") break; scanf("%d%d", &i, &j); if(str == "Query"){ printf("%d\n", query(i, j, 1, n, 1)); } else{ if(str=="Add") update(i, j, 1, n, 1); else update(i, 0-j, 1, n, 1); } } } return 0; }
-
給你一個數列,求這個數列的逆序數。這是一道求逆序數的模板題。
那麼如何通過線段樹求逆序數呢。首先我們知道一個數的逆序數的時候,就看在它的前面出現了幾個比他大的數,那麼這個數就是它的逆序數。所以,依照輸入順序,輸入一個數後a後,就查找區間 [a+1, n] 中有幾個數已經存在了,就是 a 的逆序數,然後標記 a 。找完所有數的逆序數再相加就可以了。 -
代碼
#include<iostream> #include<stdio.h> #include<string.h> using namespace std; #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 const int MAXN = 5005; int sum[MAXN<<2]={0}, pre[MAXN], ma, ans; //建樹 void build(int l, int r, int rt){ if(l == r) return ; int mid = (l+r)>>1; build(lson); build(rson); sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } //單點修改 void update(int x, int l, int r, int rt){ // cout<< x<< "--"<< l<< "--"<< r<< endl; if(l==r && l==x){ sum[rt] = 1; return ; } int mid = (l+r)>>1; if(x<=mid) update(x, lson); else update(x, rson); sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } //區間查詢 int query(int i, int j, int l, int r, int rt){ if(i>j) return 0; if(i==l && j==r) return sum[rt]; int mid = (l+r)>>1; if(j>=mid+1 && i<=mid) return query(i, mid, lson)+query(mid+1, j, rson); else if(j<=mid) return query(i, j, lson); else return query(i, j, rson); } int main(){ int n; while(~scanf("%d", &n)){ memset(sum, 0, sizeof(sum)); ma = 0; build(1, n, 1); for(int i=0; i<n; i++){ scanf("%d", &pre[i]); update(pre[i]+1, 1, n, 1); ma += query(pre[i]+2, n, 1, n, 1); } ans = ma; for(int i=0; i<n; i++){ ma += (n-pre[i]-1)-pre[i]; if(ma<ans) ans = ma; } printf("%d\n", ans); } return 0; }
-
模板題區間修改。但這道題中的區間修改與上面講到的區間修改不同,此題中的區間修改式改變區間中的值爲給定值,不是累加關係。
-
代碼
#include<iostream> #include<stdio.h> #include<string.h> #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 #define ll int using namespace std; const int MAXN = 1e5+10; ll sum[MAXN<<2]={0}, add[MAXN<<2]={0}; void push_up(int rt){ sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } void push_down(int rt, int m){ if(add[rt]){ add[rt<<1] = add[rt]; add[rt<<1|1] = add[rt]; sum[rt<<1] = (m-(m>>1))*add[rt]; sum[rt<<1|1] = (m>>1)*add[rt]; add[rt] = 0; } } void build(int l, int r, int rt){ add[rt] = 0; if(l == r){ sum[rt] = 1; return ; } int mid = (l+r)>>1; build(lson); build(rson); push_up(rt); } void update(int a, int b, ll c, int l, int r, int rt){ if(a<=l && b>=r){ sum[rt] = (r-l+1)*c; add[rt] = c; return ; } push_down(rt, r-l+1); int mid = (l+r)>>1; if(a<=mid) update(a, b, c, lson); if(b>mid) update(a, b, c, rson); push_up(rt); } int main(){ int k; scanf("%d", &k); for(int i=1; i<=k; i++){ int N, Q, x, y, z; scanf("%d%d", &N, &Q); build(1, N, 1); while(Q--){ scanf("%d%d%d", &x, &y, &z); update(x, y, z, 1, N, 1); } printf("Case %d: The total value of the hook is %d.\n", i, sum[1]); memset(sum, 0, sizeof(sum)); memset(add, 0, sizeof(add)); } return 0; }
-
模板題,單點修改+區間最值。
-
代碼
#include<iostream> #include<stdio.h> #include<string.h> #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 #define ll int using namespace std; const int MAXN = 2e5+10; ll sum[MAXN<<2]={0}; void push_up(int rt){ sum[rt] = sum[rt<<1]>sum[rt<<1|1]? sum[rt<<1]:sum[rt<<1|1]; } void build(int l, int r, int rt){ if(l == r){ scanf("%d", &sum[rt]); return ; } int mid = (l+r)>>1; build(lson); build(rson); push_up(rt); } void update(int a, int b, int l, int r, int rt){ if(l==r){ sum[rt] = b; return ; } int mid = (l+r)>>1; if(a<=mid) update(a, b, lson); else update(a, b, rson); push_up(rt); } int query(int a, int b, int l, int r, int rt){ if(a<=l && b>=r) return sum[rt]; int mid = (l+r)>>1; int x=0, y=0; if(a<=mid) x = query(a, b, lson); if(b>=mid+1) y = query(a, b, rson); return x>y? x:y; } int main(){ int n, m, a, b; char c; while(~scanf("%d%d", &n, &m)){ build(1, n, 1); while(m--){ scanf(" %c%d%d", &c, &a, &b); if(c=='Q') printf("%d\n", query(a, b, 1, n, 1)); else update(a, b, 1, n, 1); } } return 0; }
-
單點修改+區間查詢(查找到某一點)的模板題。這道題難點在如何使用線段樹,方法就是:由於海報的高都是一個單位,所以可以將海報板按單位長度分成一行一行,線段樹的每個單元區間(就是線段樹的葉節點,也是線段上的一個點)代表一行,單元區間的值表示這個行剩下的長度。因爲貼海報的時候是依據上和左優先,其次是下和右的原則,所以只需要從線段上從走向有查找,就相當於從海報板上從上向下查找,對於一行,看是否可以貼(剩餘長度是否夠)就可以,實際上是不用考慮從左向右的。所以,想明白之後,就是單點修改+區間查詢的模板題。
-
代碼
#include<iostream> #include<stdio.h> #include<string.h> #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 #define ll int using namespace std; const int MAXN = 2e5+5; ll sum[MAXN<<2]; int h, w, n, pos, x; void push_up(int rt){ sum[rt] = sum[rt<<1]>sum[rt<<1|1]? sum[rt<<1]:sum[rt<<1|1]; //保存區間最大值,當該區間最大值比海報長度小時,就肯定不貼在該區間,也就不用搜索了。 } void build(int l, int r, int rt){ if(l == r){ sum[rt] = w; return ; } int mid = (l+r)>>1; build(lson); build(rson); push_up(rt); } void update(int l, int r, int rt){ if(l==r){ sum[rt] -= x; return ; } int mid = (l+r)>>1; if(pos<=mid) update(lson); else update(rson); push_up(rt); } bool query(int l, int r, int rt){ if(l==r){ if(x<=sum[rt]){ pos = l; return true; } else return false; } int mid = (l+r)>>1; if(x<=sum[rt<<1]) return query(lson); else return query(rson); } int main(){ while(~scanf("%d%d%d", &h, &w, &n)){ h = min(h, n); build(1, h, 1); while(n--){ scanf("%d", &x); if(query(1, h, 1)){ printf("%d\n", pos); update(1, h, 1); } else printf("-1\n"); } } return 0; }
-
逆序數+離散化。這道題的突破點在於求逆序數。因爲排序時是通過相鄰元素兩兩交換完成的,所以實際上排序完成後的交換次數就是給定初始序列的逆序數。實際上就是通過線段樹求逆序數的問題了。另外就是離散化的問題。由於要排序的數據可能很大,如果只根據數據建立線段樹是行不通的,可以嘗試一下,這就還需要先進行離散化,再求逆序數。
-
代碼
#include<iostream> #include<stdio.h> #include<algorithm> #include<string.h> #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 using namespace std; const int MAX = 5e5+5; int sum[MAX<<2]; int n; __int64 num; typedef struct{ int key1, key2; int value; }node; node in[MAX]; bool cmp1(node n1, node n2){ return n1.value<n2.value; } bool cmp2(node n1, node n2){ return n1.key1<n2.key1; } void push_up(int rt){ sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } void update(int x, int l, int r, int rt){ if(l==r){ sum[rt]=1; return ; } int mid = (l+r)>>1; if(x<=mid) update(x, lson); else update(x, rson); push_up(rt); } int query(int b, int e, int l, int r, int rt){ if(b>e) return 0; if(b<=l && e>=r) return sum[rt]; int mid = (l+r)>>1; int ans = 0; if(b<=mid) ans += query(b, e, lson); if(e>=mid+1) ans += query(b, e, rson); push_up(rt); return ans; } int main(){ while(1){ scanf("%d", &n); if(n==0) break; memset(sum, 0, sizeof(sum)); num = 0; for(int i=1; i<=n; i++){ in[i].key1 = i; scanf("%d", &in[i].value); } stable_sort(in+1, in+n+1, cmp1); for(int i=1; i<=n; i++) in[i].key2 = i; stable_sort(in+1, in+n+1, cmp2); for(int i=1; i<=n; i++){ num += query(in[i].key2+1, n, 1, n, 1); update(in[i].key2, 1, n, 1); } printf("%I64d\n", num); } return 0; }
-
買票插隊,每行兩個數 a 和 b ,表示編號爲 b 的人插在了隊伍裏第 a 個人後面,問最後的隊列順序(按順序輸出每個人的編號)。這道題和奶牛排序的問題類似,由於最後一個人插進隊伍之後,這個人在隊伍裏的位置就確定了,就可以不考慮這個人對其他人的位置的影響了,所以,採用從後向前遍歷的方法來確定某一個人的位置,使用線段樹維護區間和表示該區間有多少個人沒確定位置就可以了。
-
代碼
#include<iostream> #include<stdio.h> #include<algorithm> #include<string.h> #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 using namespace std; const int MAX = 2e5+5; int sum[MAX<<2]; int n, p[MAX], v[MAX], ans[MAX], i; void push_up(int rt){ sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } void build(int l, int r, int rt){ if(l==r){ sum[rt] = 1; return ; } int mid = (l+r)>>1; build(lson); build(rson); push_up(rt); } void query(int x, int l, int r, int rt){ if(l==r){ sum[rt] = 0; ans[l] = v[i]; return ; } int mid = (l+r)>>1; if(x<=sum[rt<<1]) query(x, lson); else query(x-sum[rt<<1], rson); push_up(rt); } int main(){ while(~scanf("%d", &n)){ build(1, n, 1); for(int i=0; i<n; i++) scanf("%d%d", &p[i], &v[i]); for(i=n-1; i>=0; i--) query(p[i]+1, 1, n, 1); for(int i=1; i<=n; i++) printf("%d ", ans[i]); cout<< endl; } return 0; }
-
在直角座標系中,每一個星星佔據座標系上一個點,對於一個星星,它的 level 定義爲標繫上在它正下方、正左方以及左下方範圍內的星星的個數。由於輸入採取的原則是按照座標從左向右、從上到下的原則輸入的,忽略縱座標,只考慮橫座標,相當於在線段上的點上重複放置星星,對於星星 (x, y) 只需要查詢區間 [1, x] 內在它之前一共放置了多少個星星就可以了。
-
代碼
#include<iostream> #include<stdio.h> #include<algorithm> #include<string.h> #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 using namespace std; const int MAX = 32e3+5; int sum[MAX<<2], ans[MAX]; int n, x, y; void push_up(int rt){ sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } //void build(int l, int r, int rt){ // if(l==r){ // sum[rt] = 0; // return ; // } // int mid = (l+r)>>1; // build(lson); // build(rson); // push_up(rt); //} int query(int b, int e, int l, int r, int rt){ if(b<=l && e>=r){ return sum[rt]; } int mid = (l+r)>>1; int ans = 0; if(b<=mid) ans += query(b, e, lson); if(e>=mid+1) ans += query(b, e, rson); return ans; } void update(int x, int l, int r, int rt){ if(l==r){ sum[rt]++; return ; } int mid = (l+r)>>1; if(x<=mid) update(x, lson); else update(x, rson); push_up(rt); } int main(){ scanf("%d", &n); for(int i=0; i<n; i++){ scanf("%d%d", &x, &y); ans[query(1, x+1, 1, 32e3+1, 1)]++; update(x+1, 1, 32e3+1, 1); } for(int i=0; i<n; i++) printf("%d\n", ans[i]); return 0; }
-
約瑟夫環 + 打表 + 線段樹。
如果一個小孩兒第 n 個出隊,那麼他得到的糖果就是 n 的因子的個數。然後出隊的這個小孩兒手中有一張卡片,寫了下一個倒黴蛋的位置 a ,規則是:a > 0,從他開始順時針第 a 個人出隊; a < 0,從他開始逆時針第 a 個人出隊。
要找到糖果數最多的小孩兒,就要找n個小孩中第幾個出隊的因子數最多,也就是 1 → n 中哪個數的因子最多。
爲了避免超時,就要提前打表,找出每個數的因子個數。打表的時候也要考慮打表的方法,我採用的方法是,對於一個數,它的所有倍數因子數加一。
然後就是用線段樹模擬約瑟夫環。我採用的方法是看要出隊的小孩兒前面(包含這個小孩兒)有幾個小孩兒,從而確定要出隊的小孩兒的位置。至於有幾個小孩兒,拿出筆畫畫很容易發現規律,這裏我就直接寫出來了:
卡片上的數是正數:num = ((num-1+children[id].k-1)%sum[1] + sum[1])%sum[1]+1;
負數:num = ((num-1+children[id].k)%sum[1]+sum[1])%sum[1]+1;
這裏注意取模 +1 就好,取模是爲了模擬循環,+1 是線段樹的點是從 1 開始的。 -
代碼
#include<iostream> #include<stdio.h> #include<algorithm> #include<string.h> #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 using namespace std; const int MAX = 5e5+5; int sum[MAX<<2], ans[MAX]; int n, p, mod; struct{ int k; char name[11]; }children[MAX]; void init(){ for(int i=1; i<=MAX; i++){ ans[i]++; for(int j=i+i; j<=MAX; j+=i){ ans[j]++; } } } int the_max(int n){ int m = ans[1], j = 1; for(int i=2; i<=n; i++){ if(m<ans[i]){ m = ans[i]; j = i; } } return j; } void push_up(int rt){ sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } void build(int l, int r, int rt){ if(l==r){ sum[rt] = 1; return ; } int mid = (l+r)>>1; build(lson); build(rson); push_up(rt); } int query(int x, int l, int r, int rt){ if(l==r) return r; int mid = (l+r)>>1; if(x<=sum[rt<<1]) query(x, lson); else query(x-sum[rt<<1], rson); } void update(int x, int l, int r, int rt){ if(l==r && l==x){ sum[rt] = 0; return ; } int mid = (l+r)>>1; if(x<=mid) update(x, lson); else update(x, rson); push_up(rt); } int main(){ init(); while(~scanf("%d%d", &n, &p)){ for(int i=1; i<=n; i++) scanf("%s%d", children[i].name, &children[i].k); build(1, n, 1); int k = the_max(n), id = p, num = p; for(int i=1; i<k; i++){ update(id, 1, n, 1); if(sum[1]==0) break; if(children[id].k<0) num = ((num-1+children[id].k)%sum[1]+sum[1])%sum[1]+1; else num = ((num-1+children[id].k-1)%sum[1] + sum[1])%sum[1]+1; id = query(num, 1, n, 1); } printf("%s %d\n", children[id].name, ans[k]); } return 0; }