Removing Stones (牛客多校) (启发式分治 .二分)
题目大意:
- 给出n堆石头, 给出每堆石头的数量, 问有多少对合法的 l,r
- 使得L,R 区间内的最大值的*2 < =区间总和
思路:
- 从最大值入手, ->为了方便处理 就利用分治 去 处理出当前的最大值要考虑区间范围是那些
- 首先通过st 表来预处理 出 l,r 的最大值的位置
- 分治 dfs(l,r) 判断当前有多少个符合的区间时, 可以去枚举 左端点或者右端点的 边界值 (谁范围小选谁),然后再去二分另外一个值, ()
- 然后在把区间分为 左右2个部分即可
#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; }
#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 ....) 关于区间点对问题,
- 通过极值来吧区间分成2部分(递归处理)来做, 关键在于思考如何利用 当前区间的一半时间复杂度,或者尽可能小的时间复杂度去处理问题, 目前可以接受的时候 一半*ln(一半)