2019牛客暑期多校训练营(第三场)----G-Removing Stones

首先发出题目链接:
链接:https://ac.nowcoder.com/acm/contest/883/G
来源:牛客网
涉及:ST表,分治

点击这里回到2019牛客暑期多校训练营解题—目录贴


题目如下
在这里插入图片描述
在这里插入图片描述
n n n 堆石头可以看成 n n n 个数的序列,序列中每次将任意两个数减少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的数量假设为 n 1 n_1 n1,子串2可以认为原串为 a [ 1 ] a[1] a[1] a [ 4 ] a[4] a[4] 的子串1的数量 n 2 n_2 n2(最开始原串默认为 a [ 1 ] a[1] a[1] a [ 10 ] a[10] a[10]),子串3可以认为是原串为 a [ 6 ] a[6] a[6] a [ 10 ] a[10] a[10] 的子串1的数量 n 3 n_3 n3。那么原串所有满足条件的子串的数量为
n 1 + n 2 + n 3 n_1+n_2+n_3 n1+n2+n3

假设原串最大值位置为 i i i,左边界为 l l l,右边界为 r r r,用一个函数 dfs(int l, int r) 来求原串为 a [ l ] a[l] a[l] a [ r ] a[r] a[r] 的满足条件的子串的数量,那么答案可以认为是 d f s ( 1 , n ) dfs(1, n) dfs(1,n),且递推式为
d f s ( l , r ) = n 1 + d f s ( l , i − 1 ) + d f s ( i + 1 , r ) dfs(l,r)=n_1+dfs(l,i-1)+dfs(i+1,r) dfs(l,r)=n1+dfs(l,i1)+dfs(i+1,r)

其中:
   n 1 n_1 n1 是相对于原串为 a [ l ] a[l] a[l] a [ r ] a[r] a[r] 的子串1的数量。

   d f s ( l , i − 1 ) dfs(l,i-1) dfs(l,i1) 是相对于原串为 a [ l ] a[l] a[l] a [ r ] a[r] a[r] 的子串2的数量,也可以认为是相对于原串为 a [ l ] a[l] a[l] a [ i − 1 ] a[i-1] a[i1] 的子串1的数量。

   d f s ( l , i − 1 ) dfs(l,i-1) dfs(l,i1) 是相对于原串为 a [ l ] a[l] a[l] a [ r ] a[r] a[r] 的子串3的数量,也可以认为是相对于原串为 a [ i + 1 ] a[i+1] a[i+1] a [ r ] a[r] a[r] 的子串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的数量

可以枚举左边界然后二分找右边界,或者枚举右边界二分找左边界,用前缀和 s u m sum sum 数组来判断每次二分的结果。

此时就要看最大值的位置,如果最大值的位置靠近原序列左方就枚举左边界二分找右边界的范围;如果最大值位置靠近元素列右方就枚举右边界二分找左边界。

下图是枚举左边界二分找右边界示例图,对于每一个左边界 ( l l l m a x max max 范围) a [ i ] a[i] a[i],二分查找右边界( m a x max max r r r 范围)合法与不合法的分界线 a [ k ] a[k] a[k] a [ k ] a[k] a[k]本身也是合法的右边界),可以证明 a [ k ] a[k] a[k] a [ r ] a[r] a[r] 的所有值都可以为合法右边界(总和越大,总和的一半也越大那么 a [ m a x ] a[max] a[max] 越不可能超过总和的一半)。
在这里插入图片描述
则以 a [ i ] a[i] a[i] 为左边界的合法的子串1数量为
r − k + 1 r-k+1 rk+1

但是有一个特殊情况就是对于某个左边界不存在合法右边界;即
a [ m a x ] > ( ( s u m [ r ] − s u m [ i − 1 ] ) > > 1 ) a[max] > ((sum[r] - sum[i-1]) >> 1) a[max]>((sum[r]sum[i1])>>1)如果左边界为 a [ i ] a[i] a[i] 时已经不存在合法右边界,那么对于所有的 a [ i + 1 ] a[i+1] a[i+1] a [ m a x ] a[max] a[max] 左边界,都不会存在合法右边界,这种情况要在二分查找之前就要判断来减少复杂度。枚举右边界找左边界类似情况。

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;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值