bzoj1558 [JSOI2009]等差數列
傳送門:http://www.lydsy.com/JudgeOnline/problem.php?id=1558
【題解】
這題惡心死人了啊。。
網絡上題解很多都是看代碼看代碼。。真是太不負責任了。。我這裏詳細說一下吧。。
題解在代碼下面。
# include <stdio.h> # include <string.h> # include <iostream> # include <algorithm> // # include <bits/stdc++.h> using namespace std; typedef long long ll; typedefView Codelong double ld; typedef unsigned long long ull; const int M = 5e5 + 10; const int mod = 1e9+7; # define RG register # define ST static int n, a[M], b[M]; struct pa { int s, ll, rr, sz; // 連續(包括中間散的),左散,右散 long long l, r; pa() {} pa(int s, int ll, int rr, int sz, long long l, longlong r) : s(s), ll(ll), rr(rr), sz(sz), l(l), r(r) {} friend pa operator + (pa a, pa b) { pa c; int f = (a.r == b.l); c.s = a.s + b.s; c.sz = a.sz + b.sz; c.l = a.l, c.r = b.r; if(a.s == 0 && b.s == 0) { if(!f) c.ll = c.rr = c.sz; else{ c.ll = a.ll-1; c.rr = b.rr-1; ++c.s; } return c; } if(a.s == 0) { c.rr = b.rr; if(!f) c.ll = a.sz + b.ll; else { c.ll = a.ll-1; if(b.ll > 0) c.s += (b.ll-1)/2 + 1; } return c; } if(b.s == 0) { c.ll = a.ll; if(!f) c.rr = a.rr + b.sz; else { c.rr = b.rr-1; if(a.rr > 0) c.s += (a.rr-1)/2 + 1; } return c; } c.ll = a.ll, c.rr = b.rr; if(a.rr == 0 && b.ll == 0) { if(f) --c.s; return c; } if(a.rr == 0) { if(f) c.s += (b.ll-1)/2; else c.s += b.ll/2; return c; } if(b.ll == 0) { if(f) c.s += (a.rr-1)/2; else c.s += a.rr/2; return c; } int d = (a.rr + b.ll)/2; if(f) d = min(d, 1 + (a.rr-1)/2 + (b.ll-1)/2); c.s += d; return c; } }; namespace SMT { pa w[M]; ll tag[M]; # define ls (x<<1) # define rs (x<<1|1) inline void up(int x) { if(!x) return; if(!rs) {w[x] = w[ls]; return;} if(!ls) {w[x] = w[rs]; return;} w[x] = w[ls] + w[rs]; } inline void pushtag(int x, ll d) { w[x].l += d, w[x].r += d; tag[x] += d; } inline void down(int x) { if(!x) return; if(!tag[x]) return; pushtag(ls, tag[x]); pushtag(rs, tag[x]); tag[x] = 0; } inline void build(int x, int l, int r) { tag[x] = 0; if(l == r) { w[x] = pa(0, 1, 1, 1, b[l], b[l]); return ; } int mid = l+r>>1; build(ls, l, mid); build(rs, mid+1, r); up(x); } inline void edt(int x, int l, int r, int L, int R, ll d) { if(L <= l && r <= R) { pushtag(x, d); return ; } down(x); int mid = l+r>>1; if(L <= mid) edt(ls, l, mid, L, R, d); if(R > mid) edt(rs, mid+1, r, L, R, d); up(x); } inline pa query(int x, int l, int r, int L, int R) { if(L <= l && r <= R) return w[x]; down(x); int mid = l+r>>1; if(R <= mid) return query(ls, l, mid, L, R); else if(L > mid) return query(rs, mid+1, r, L, R); else return query(ls, l, mid, L, mid) + query(rs, mid+1, r, mid+1, R); } inline void debug(int x, int l, int r) { printf("x=%d, l=%d, r=%d: sum = %d, size = %d, left = %d, right = %d, lnum = %lld, rnum = %lld\n", x, l, r, w[x].s, w[x].sz, w[x].ll, w[x].rr, w[x].l, w[x].r); if(l==r) return; down(x); int mid = l+r>>1; debug(ls, l, mid); debug(rs, mid+1, r); } } int main() { int Q, l, r, a1, d; char opt[23]; pa t; cin >> n; for (int i=1; i<=n; ++i) scanf("%d", a+i); for (int i=1; i<n; ++i) b[i] = a[i+1] - a[i]; // for (int i=1; i<n; ++i) printf("%d ", b[i]); puts(""); cin >> Q; if(n == 1) { while(Q--) { scanf("%s", opt); if(opt[0] == ‘A‘) scanf("%*d%*d%*d%*d"); if(opt[0] == ‘B‘) {scanf("%*d%*d"); puts("1");} } return 0; } SMT::build(1, 1, n-1); // SMT::debug(1, 1, n-1); while(Q--) { scanf("%s", opt); if(opt[0] == ‘A‘) { scanf("%d%d%d%d", &l, &r, &a1, &d); if(l != 1) SMT::edt(1, 1, n-1, l-1, l-1, a1); if(l <= r-1) SMT::edt(1, 1, n-1, l, r-1, d); if(r != n) SMT::edt(1, 1, n-1, r, r, -1ll * (r-l)*d - a1); } if(opt[0] == ‘B‘) { scanf("%d%d", &l, &r); if(l == r) puts("1"); else { t = SMT::query(1, 1, n-1, l, r-1); int ans = (r-l+1+1)/2; if(t.s == 0) printf("%d\n", ans); else { ans = min(ans, t.s + (t.ll+1)/2 + (t.rr+1)/2); printf("%d\n", ans); } } } // SMT::debug(1, 1, n-1); } return 0; }
對就是這裏這裏是題解啦!
首先一段加等差數列。這個可以用線段樹直接維護,但是為了詢問方便,我們選擇線段樹維護差分數組。
比如:
n=6, a[]={1,2,4,7,11,16}
那麽維護的就是b[]={1,2,3,4,5},b[i] = a[i+1] - a[i]
對於b建立線段樹進行維護。
由於我們發現n=1沒有差分。。要特判下。
那麽現在我們考慮修改:在[a,b]上加首項a1公差d的等差數列。由於我們維護的是差分數組,這個就很好辦了。
假裝[a,b]很長,那麽中間的部分差分增加的是公差d(前一個+x+d,後一個+x+d+d)。
稍微推導下(這部分可以自己舉例子,自行算算,特別是邊界條件),即可得到:
修改:b[l-1] += a1, b[l...r-1] += d, b[r] -= (r-l)*d+a1
這裏需要判斷:如果我們修改了區間[1,?],那麽b[l-1]不需要改,因為第一個地方沒有差分。
如果我們修改了區間[x,x],就不需要進行第二個修改。等等。
至於第三個減,是為了維護後面差分穩定,容易看出a[r] += (r-l)*d+a1,所以a[r+1]-a[r]要減少(r-1)*d+a1
我們討論完了修改我們開始討論詢問吧。
詢問求的是最少分成多少個等差數列段。由於是段,連續的,就比序列好辦多了。
這不是簡單求[l,r]區間的差分數組有多少個連續的段的問題。
因為,比如很簡單的例子:
差分數組為b[]={1,2,3,4},a[]={x,x+1,x+3,x+6,x+10}
那麽答案不是4段,而是3段,為什麽?因為{x,x+1}和{x+3,x+6}和{x+10}都是等差,所以我們要判斷2個數構成等差這個情況。
所以我們要多維護東西。
首先可以確定的是,一個區間的中間部分如果確定是哪些等差數列,以後是不會改變的。
所以我們維護:
1. 區間中的等差數列段數。
2. 區間左邊剩下的零散數的個數,以及右邊剩下的零散數的個數
3. 區間左邊的數是什麽,區間右邊的數是什麽
4. 區間大小。
假設區間為[l,r],那麽我們最後的答案就是線段樹查詢[l,r-1](因為差分過了所以要-1)出來後:
1. 要麽是(r-l+1+1)/2(r-l+1為區間長度,這種分法是兩個數兩個數分)
2. 要麽是詢問出來的區間中的等差數列段數加上區間左邊剩下的零散數的個數,以及右邊剩下的零散數的個數按照1中方法分成等差數列的個數。
至於兩邊的個數怎麽對應到段上,可以通過畫幾個例子來解決。
好了現在只要考慮線段樹內和查詢時,數據合並了。
1 struct pa { 2 int s, ll, rr, sz; 3 // 連續(包括中間散的),左散,右散 4 long long l, r; 5 pa() {} 6 pa(int s, int ll, int rr, int sz, long long l, long long r) : s(s), ll(ll), rr(rr), sz(sz), l(l), r(r) {} 7 friend pa operator + (pa a, pa b) { 8 pa c; int f = (a.r == b.l); 9 c.s = a.s + b.s; c.sz = a.sz + b.sz; 10 c.l = a.l, c.r = b.r; 11 if(a.s == 0 && b.s == 0) { 12 if(!f) c.ll = c.rr = c.sz; 13 else { 14 c.ll = a.ll-1; 15 c.rr = b.rr-1; 16 ++c.s; 17 } 18 return c; 19 } 20 if(a.s == 0) { 21 c.rr = b.rr; 22 if(!f) c.ll = a.sz + b.ll; 23 else { 24 c.ll = a.ll-1; 25 if(b.ll > 0) c.s += (b.ll-1)/2 + 1; 26 } 27 return c; 28 } 29 if(b.s == 0) { 30 c.ll = a.ll; 31 if(!f) c.rr = a.rr + b.sz; 32 else { 33 c.rr = b.rr-1; 34 if(a.rr > 0) c.s += (a.rr-1)/2 + 1; 35 } 36 return c; 37 } 38 39 c.ll = a.ll, c.rr = b.rr; 40 41 if(a.rr == 0 && b.ll == 0) { 42 if(f) --c.s; 43 return c; 44 } 45 if(a.rr == 0) { 46 if(f) c.s += (b.ll-1)/2; 47 else c.s += b.ll/2; 48 return c; 49 } 50 if(b.ll == 0) { 51 if(f) c.s += (a.rr-1)/2; 52 else c.s += a.rr/2; 53 return c; 54 } 55 56 int d = (a.rr + b.ll)/2; 57 if(f) d = min(d, 1 + (a.rr-1)/2 + (b.ll-1)/2); 58 59 c.s += d; 60 61 return c; 62 } 63 };
我們看上述代碼就是線段樹內結構體以及怎麽合並。
首先sz, l,r都直接合並。
然後要考慮的就是ll和rr還有s要怎麽合並的問題了。
先處理左邊/右邊沒有連續段的問題(都是散的)
分三類:都沒有,左沒有,右沒有
這部分直接看,是11~37行
現在左邊/右邊都有連續段了,要處理的是:
左邊段緊挨著邊界這種情況,也就是ll=0或rr=0這種情況。
分三類:a.rr=0&&b.ll=0,a.rr=0,b.ll=0
第一類顯然兩個段如果a.r=b.l的話就能直接合並成一個段了,否則就正常合並,在第41~44行。
後兩類,如果a.r=b.l的時候,剩下的一個(大於0的b.ll或a.rr)就可以少一個數進行劃分了。在第45~54行。
否則,就按照正常的劃分方法:要麽全部劃分成2個2個的,要麽把中間相等的提出來,左右劃分。
懶得討論提出來是不是一定優了。。就取min唄
然後……呼終於說完了
這題挺好的。。細節賊多
bzoj1558 [JSOI2009]等差數列