首先發出題目鏈接:
鏈接:https://ac.nowcoder.com/acm/contest/883/G
來源:牛客網
涉及:ST表,分治
點擊這裏回到2019牛客暑期多校訓練營解題—目錄貼
題目如下
堆石頭可以看成 個數的序列,序列中每次將任意兩個數減少1,如果能讓這段序列的所有數都會減少到0,那麼必須保證序列中最大那個數不大於所有數之和的二分之一(題目所說總和如果爲奇數那就把最小那個數減1恰好是爲了滿足這個條件)
於是題目的意思就變成了:找到原序列滿足最大值不大於所有值之和的二分之一的子串的數量。
可以先找到原序列的最大值位置,可以用ST表來實現。
ST表模板
int st[maxn][25];
int lg[300005] = {-1};
void init(){
for(int i = 1; i <= 300005; i++) lg[i] = lg[i/2] + 1;//注意N的範圍是1~300000,所以這裏lg數組要開300005這麼多
for(int i = 1; i <= n; i++){
st[i][0] = i;
}
for(int j = 1; (1 << j) <= n; j++){
for(int i = 1; i + (1 << (j-1)) <= n; i++){
st[i][j] = (a[st[i][j-1]] > a[st[i+(1<<(j-1))][j-1]])? st[i][j-1]: st[i+(1<<(j-1))][j-1];
}
}
}
int query_max_place(int l, int r){//求區間l~r的最大值位置
int k = lg[r-l+1];
return (a[st[l][k]] > a[st[r-(1<<k)+1][k]])? st[l][k]: st[r-(1<<k)+1][k];
}
如下圖所示
如果要找到滿足條件的子串,那麼子串可能出現三種情況
1.子串包含原串的最大值(或者以最大值位置爲左右邊界),這種滿足條件的子串設爲子串1
2.子串位於原串最大值的左邊,這種滿足條件的子串設爲子串2
3.子串位於原串最大值的右邊,這種滿足條件的子串設爲子串3
子串1的數量假設爲 ,子串2可以認爲原串爲 到 的子串1的數量 (最開始原串默認爲 到 ),子串3可以認爲是原串爲 到 的子串1的數量 。那麼原串所有滿足條件的子串的數量爲
假設原串最大值位置爲 ,左邊界爲 ,右邊界爲 ,用一個函數 dfs(int l, int r)
來求原串爲 到 的滿足條件的子串的數量,那麼答案可以認爲是 ,且遞推式爲
其中:
是相對於原串爲 到 的子串1的數量。
是相對於原串爲 到 的子串2的數量,也可以認爲是相對於原串爲 到 的子串1的數量。
是相對於原串爲 到 的子串3的數量,也可以認爲是相對於原串爲 到 的子串1的數量。
dfs函數的僞代碼如下
ll ans = 0;//答案
void dfs(int L, int R){
if(L >= R) return;//左邊界要在右邊界的左邊
int k = query_max_place(L, R);//ST表求最大值位置
ans += (相對於原串爲a[L]~a[R]的子串1的數量);
dfs(L, k-1);//答案加上子串2的數量
dfs(k+1, R);//答案加上子串3的數量
return;
}
如何獲得相對於原串爲a[L]~a[R]的子串1的數量
可以枚舉左邊界然後二分找右邊界,或者枚舉右邊界二分找左邊界,用前綴和 數組來判斷每次二分的結果。
此時就要看最大值的位置,如果最大值的位置靠近原序列左方就枚舉左邊界二分找右邊界的範圍;如果最大值位置靠近元素列右方就枚舉右邊界二分找左邊界。
下圖是枚舉左邊界二分找右邊界示例圖,對於每一個左邊界 ( 到 範圍),二分查找右邊界( 到 範圍)合法與不合法的分界線 (本身也是合法的右邊界),可以證明 到 的所有值都可以爲合法右邊界(總和越大,總和的一半也越大那麼 越不可能超過總和的一半)。
則以 爲左邊界的合法的子串1數量爲
但是有一個特殊情況就是對於某個左邊界不存在合法右邊界;即
如果左邊界爲 時已經不存在合法右邊界,那麼對於所有的 到 左邊界,都不會存在合法右邊界,這種情況要在二分查找之前就要判斷來減少複雜度。枚舉右邊界找左邊界類似情況。
ll ans = 0;
void dfs(int L, int R){//求解l~r範圍內滿足條件的子串的數量
if(L >= R) return;//左邊界要在右邊界的左邊
int k = query_max_place(L, R);//ST表求最大值位置
if(R + L > 2 * k){//判斷最大值位置靠近左邊還是右邊,下面是靠近左邊的處理方式
for(int i = L; i <= k; i++){//枚舉左邊界
if(a[k] > ((sum[R] - sum[i-1]) >> 1)) break;//判斷是否存在合法右邊界
int l = k, r = R;//二分區域
while(l < r){//二分
int mid = (l + r) >> 1;
if(a[k] > ((sum[mid] - sum[i-1]) >> 1)){
l = mid + 1;
}
else r = mid;
}
ans += 1ll * (R - l + 1);//子串數量加到ans中
}
}
else{ //下面是最大值靠近右邊界的處理,與靠近左邊界類似
for(int i = R; i >= k; i--){
if(a[k] > ((sum[i] - sum[L-1]) >> 1)) break;
int l = L, r = k;
while(l < r){
int mid = (l + r + 1) >> 1;
if(a[k] > ((sum[i] - sum[mid-1]) >> 1)){
r = mid - 1;
}
else l = mid;
}
ans += 1ll * (l - L + 1);
}
}
dfs(L, k-1);//答案加上子串2的數量
dfs(k+1, R);//答案加上子串3的數量
return;
}
代碼如下:
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn = 3e5+5;
int st[maxn][25];//st表
int n, cas;//題目所給變量
ll ans = 0;//答案
ll a[maxn], sum[maxn];//a爲原序列,sum爲前綴和序列
int lg[300005] = {-1};//st表需要的lg數組
void init(){//初始化st表
for(int i = 1; i <= n; i++){
st[i][0] = i;
}
for(int j = 1; (1 << j) <= n; j++){
for(int i = 1; i + (1 << (j-1)) <= n; i++){
st[i][j] = (a[st[i][j-1]] > a[st[i+(1<<(j-1))][j-1]])? st[i][j-1]: st[i+(1<<(j-1))][j-1];
}
}
}
int query_max_place(int l, int r){//st表求最大值位置
int k = lg[r-l+1];
return (a[st[l][k]] > a[st[r-(1<<k)+1][k]])? st[l][k]: st[r-(1<<k)+1][k];
}
void dfs(int L, int R){//求解l~r範圍內滿足條件的子串的數量
if(L >= R) return;//左邊界要在右邊界的左邊
int k = query_max_place(L, R);//ST表求最大值位置
if(R + L > 2 * k){//判斷最大值位置靠近左邊還是右邊,下面是靠近左邊的處理方式
for(int i = L; i <= k; i++){//枚舉左邊界
if(a[k] > ((sum[R] - sum[i-1]) >> 1)) break;//判斷是否存在合法右邊界
int l = k, r = R;//二分區域
while(l < r){//二分
int mid = (l + r) >> 1;
if(a[k] > ((sum[mid] - sum[i-1]) >> 1)){//判斷是否滿足最大值不超過當前區域總和的一半
l = mid + 1;
}
else r = mid;
}
ans += 1ll * (R - l + 1);//子串數量加到ans中
}
}
else{ //下面是最大值靠近右邊界的處理,與靠近左邊界類似
for(int i = R; i >= k; i--){
if(a[k] > ((sum[i] - sum[L-1]) >> 1)) break;
int l = L, r = k;
while(l < r){
int mid = (l + r + 1) >> 1;
if(a[k] > ((sum[i] - sum[mid-1]) >> 1)){
r = mid - 1;
}
else l = mid;
}
ans += 1ll * (l - L + 1);
}
}
dfs(L, k-1);//答案加上子串2的數量
dfs(k+1, R);//答案加上子串3的數量
return;
}
int main(){
for(int i = 1; i <= 300005; i++) lg[i] = lg[i/2] + 1;//獲得lg數組
cin >> cas;
while(cas--){
ans = 0;
scanf("%d", &n);
for(int i = 1; i <= n; i++){//獲得前綴和數組
scanf("%d", &a[i]);
sum[i] = sum[i-1] + a[i];
}
init();//初始化st表
dfs(1, n);//求1~n範圍內合法的子串數量
cout << ans << endl;
}
return 0;
}