--- tags: Question tutorial --- # 第K小區間和 ## **注意!!作法二比較好** - 改成第K大區間和 - 注意到前綴和、後綴和為遞增 - 把`lower_bound` 改成線性 ## 先備知識 一個長度為$N$的值陣列會有$\frac{N(N + 1)}{2}$ 個區間和 ## NA20% 就窮舉 ## NA80% 任一個區間都可以被表示為$\implies$ `總和 - (前綴和 + 後綴和)` 因為`總和`為定值所以若有一個長為$N$的區間,其區間和為第$K$小,則其`前綴和 + 後綴和`為第$\frac{N * (N + 1)}{2} - K + 1 大 \implies 令他為第T大問題$ 那要如何找到第$T$大的`前綴和 + 後綴和`呢? ```cpp= K = (N * (N + 1) / 2) - K + 1; ``` | 2 | 4 | 3 | 3 | | -------- | -------- | -------- | ------ | **p_f(前綴和)** | 0 | 2 | 6 | 9 | 12 | | -------- | -------- | -------- | ------ | ------ | **p_b(後綴和)** | 0 | 3 | 6 | 10 | 12 | | --- | --- | --- | --- | --- | ### 想法 想不到如何找 $\implies$ 簡化題目。 - 若`前綴和 + 後綴和`為`m`,那他會比幾個區間和大? $\quad(0 \leq m \leq sum)$ - 他比`T`個區間和大,那這些區間中**最大**的是否為第`T`大? - 那這樣是不是可以對`m`二分搜求出第`T`大? 這邊放程式碼,想一想為什麼正確 **<center>找出比`m`的值小的區間和總數。</center>** ```cpp= #define V vector #define ALL(x) x.begin(), x.end() void find(V<ll> &p_f, V<ll> &p_b, ll m) { ans = 0, mx = -1; for(int i = 0; i <= N; i++) { if(m < p_f[i]) return; ll x = m - p_f[i]; auto p = lower_bound(ALL(p_b), x); p--; ans += p - p_b.begin() + 1; if(*p + p_f[i] > mx) mx = *p + p_f[i]; } } ``` **<center>對`m`做二分搜</center>** ```cpp= while(1) { ll m = (l + r) >> 1; find(p_f, p_b, m); if(ans == K || r == l) { cout << sum - mx << endl; return; } if(ans < K) l = m + 1; else r = m; } ``` 時間複雜度為$O(\lg sum \times N\lg N)$ ## AC 發現在$N$長度為$10^6$時,$sum$的值約為$2^{32}$,上面的時間複雜度 $\approx 2 * 10^8$,會$TLE$ 觀察一下`find`函式裡面找的過程。 可以發現,`p - p_b.begin() + 1` 的值只會越來越小 $\implies p_f[i]的值越來越大$。 所以可以不用每次都接著二分搜,接下去找就好!! 神奇的事情發生了~ 因為是接著下去找,所以`find`函式最多只會把`p_f`, `p_b`都遍歷過一遍 $\implies$ `find`時間複雜度達到$O(N)!!!$ **<center>改良版`find`函數</center>** ```cpp= void find(V<ll> &p_f, V<ll> &p_b, ll m) { ans = 0, mx = -1; int cur_pos = N; for(int i = 0; i <= N; i++) { if(m < p_f[i]) return; ll x = m - p_f[i]; while(p_b[cur_pos] >= x) cur_pos--; ans += cur_pos + 1; if(p_b[cur_pos] + p_f[i] > mx) mx = p_b[cur_pos] + p_f[i]; } } ``` 時間複雜度為$(\lg sum \times N)$ # 作法二( 前綴和 ) ## 性質 - $sum[l, r] = sum[1, r]-sum[1,l-1]$ - 前綴和遞增 ## 觀察 **M具有單調性** 可以發現,如果要找到一個大於等於$M$(他是一個數字)的區間和的個數。 在$M$遞增的情況下它也會遞增,所以它具有單調性 **前綴和有單調性** 對於一個前綴合$X$($sum[1,r]$),如果想要讓區間和大於$M$,可以發現,符合的條件($sum[1,r]-sum[1,l-1] \geq M$),其中的$l$必定會出現在前面。 **轉換** $(區間和 \leq M) = (全部可能性) - (區間和 > M)$ ## 作法(NA80%) 因為$M$有單調性,我們不妨先對它做二分搜。 :::success 為什麼可以做二分搜,可以思考一下。 如果對區間和做排序的話,可以發現$M$其實就是在$\frac{N(N+1)}{2}$的區間上做二分搜,若$M$越大,獲得的區間個數也會越大。 所以其實我們獲得$M$之後動態的計算個數。 ::: 所以我們可以對$M$做二分搜,然後計算有幾個區間和比它大。 複雜度$O(logC \times N lg N)$ ## 優化 可以發現二分搜的時候,界線其實是一直遞增的,所以用一個雙指針維護就好了 :::spoiler code `郭勝威` ```cpp= #include<bits/stdc++.h> using namespace std; #define F first #define S second #define ALL(x) begin(x), end(x) #define iter(x, sz) (x), (x)+sz typedef unsigned long long ull; typedef pair<int,int> pii; typedef long long ll; constexpr int mxN = 1e5 + 20; int main () { ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); int N; ll K; cin >> N >> K; --K; auto cint = [](int &a) {cin >> a;}; vector< int > vc(N); for_each( ALL(vc), cint ); ll l = 0, r = 1e15; while( r-l > 1 ) { ll m = (r+l)>>1; ll lsum = 0, rsum = 0; int l_pos = 0, r_pos = 0; ll cnt = 0; for_each(ALL(vc), [&] (int &a ) { rsum += a; while( rsum - lsum >= m ) { lsum += vc[l_pos]; ++l_pos; } cnt += (r_pos - l_pos+1); r_pos++; } ); if( cnt > K ) r = m; else l = m; } cout << l << '\n'; } ``` ::: :::spoiler code `劉冠甫` ```cpp= #include<iostream> #include<vector> using namespace std; #define ll long long int main(){ cin.tie(0) -> sync_with_stdio(0); ll n, k; cin >> n >> k; ll v[n + 1]; v[0] = 0; for (int i = 1; i <= n; i ++){ int p; cin >> p; v[i] = v[i - 1] + p; } ll l = 0, r = 1e16; while (r - l > 1){ ll m = (r + l) >> 1; ll cnt = 0; int fi = 1, si = 0; for (; fi <= n; fi ++){ while (v[fi] - v[si] >= m){ si ++; } cnt += fi - si; } if (cnt >= k) r = m; else l = m; } ll cnt = 0; int fi = 1, si = 0; for (; fi <= n; fi ++){ while (v[fi] - v[si] > l){ si ++; } cnt += fi - si; } if (cnt < k - 1) l = r; cout << l << '\n'; } ``` :::