洛谷 P9683 A Certain Forbidden Index 题解

题目链接:\(\color{Purple}\texttt{P9683 A Certain Forbidden Index}\)

填坑。提供一个相对好写的做法。

考虑把一堆不交的区间绑在一起问(即先询问这些区间的并,判断答案是否在里面):对于一个区间,能与它绑在一起的区间最多只有 \(1\) 个是与它同一层的。

于是我们想到一个比较原始的做法:因为 \([1,2^k]\) 没有任何区间可以合并,所以放最后作为单独一块问;从线段树的顶端按照 BFS 序往下询问,如果该区间 \([l,r]\) 没有进行过标记(记这种区间为"初始区间 "),按以下方式处理:标记它,如果它是作为左儿子被问到的(反之同理),那么就不断往右找下一层且它们的并集是一个区间 的区间 \([l_1,r_1]\)(即满足 \(l_1=r+1\)),然后 \([l,r]\leftarrow[l_1,r_1]\),对 \([l_1,r_1]\) 进行标记;这样能获得不少的块并且答案已经很优。加上对 \(k=1\) 的特判可以做到 \(63\mathrm{pts}\):笔者考场上使用的是该做法,已经得到了一个非常可观的分数。

注:对于这种做法,如果对块内相邻元素连边,在整棵线段树上这些边就可以构成很多交叉的 X 型(读者可以自行理解一下)。

但是此时答案还不是最优的。考虑是否可以合并一些区间:我们观察到,对于一些左 / 右端点在正中间的作为右 / 左儿子进行处理的区间,是可以将它所在的块和同一层那个唯一可以合并的区间所在的块合并的。例如,\(k=4\) 时,区间 \([7,8]\) 与区间 \([9,10]\) 所在的块可以合并。处理的过程中考虑一下这种情况。

事实上我们可以观察到,对于合并同一层的这种情况,我们只需处理 \([l,r]\) 满足 \(r<2^k\) 且作为右儿子 询问到的"初始区间"所在的块和它右边那个同层的且可合并的区间所在的块即可。证明显然。

询问块内元素的过程可以不用像其他题解一样用二分,因为我们观察到如果一个块的大小如果不超过 \(2\),那么二分跟朴素做法的没有区别------实际上块越小两种做法的区别越小。又因为是按照 BFS 序从上往下问,感性理解一下就可以得知越往下问块越小而且数量越多。实测可以获得 \(100\mathrm{pts}\)。

如果不放心可以把所有块全部预处理出来然后按照块的大小降序排序询问。但是这个做法会在 \(k=14\) 的时候超时,所以选用前者。

示例代码(\(100\mathrm{pts}\)):

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
int query(int,int);
pii solve(int k){
  vector<tuple<int,int,int,int> > q;
  function<void(int,int,int)> dfs=[&](int l,int r,int w){
    if(l==r)return;
    q.emplace_back(l,l+r>>1,w-1,0);
    q.emplace_back(l+r+1>>1,r,w-1,1);
    dfs(l,l+r>>1,w-1);
    dfs(l+r+1>>1,r,w-1);
  }; // 按照 BFS 序处理出所有区间遍历顺序,0/1 表示左 / 右儿子
  dfs(1,1<<k,k);
  set<pii> t; t.emplace(1,1<<k);
  vector<tuple<vector<pii>,int,int,int,int> > s;
  for(auto [l,r,w,b]:q){
    if(t.find(make_pair(l,r))==t.end()){
      vector<pii> v; int p=(b?l:r);
      for(int i=w-1;i>=0;i--){
        v.emplace_back(b?p-(1<<i):p+1,b?p-1:p+(1<<i));
        b?p-=(1<<i):p+=(1<<i);
      } // 处理块
      if(b){
        bool b2=false;
        if(r<(1<<k)){
          b2=true,p=r;
          for(int i=w;i>=0;i--){
            v.emplace_back(p+1,p+(1<<i));
            p+=(1<<i);
          }
        } // 可以与同一层的合并
        if(query(l-(1<<w)+1,b2?(r<<1)-l+(1<<w):r)){
          // 询问是否在整个区间,下同
          for(auto [l1,r1]:v)
            if(query(l1,r1))return make_pair(l1,r1);
          // 询问单个区间,下同
          return make_pair(l,r);
          // 不在上述区间就是最后一个,下同
        }
      }
      else if(query(l,r+(1<<w)-1)){
        for(auto [l1,r1]:v)
          if(query(l1,r1))return make_pair(l1,r1);
        return make_pair(l,r);
      }
      for(auto [l1,r1]:v)t.emplace(l1,r1);
      t.emplace(l,r); // 使用 std::set 打标记
    }
  }
  return make_pair(1,1<<k); // 都不是上述区间
}
相关推荐
邓校长的编程课堂9 天前
少儿编程进入义务教育课程:培养信息科技素养的新政策解读
科技·编程语言·少儿编程·信息学竞赛·科技特长生·义务教育
Physics2123035 个月前
杂题选讲 #1:二分图边着色
信息学竞赛
少儿编程乔老师7 个月前
每周一算法:旋转游戏
c++·算法·青少年编程·迭代加深·信息学竞赛
少儿编程乔老师7 个月前
每周一算法:迭代加深A*
c++·算法·青少年编程·迭代加深·信息学竞赛
少儿编程乔老师10 个月前
NOIP2003提高组T1:神经网络
c++·算法·青少年编程·广度优先·信息学竞赛
少儿编程乔老师10 个月前
每周一算法:数独游戏
c++·算法·青少年编程·深度优先·信息学竞赛
少儿编程乔老师10 个月前
每周一算法:倍增法求区间最大最小值(RMQ)
c++·算法·青少年编程·信息学竞赛
少儿编程乔老师10 个月前
每周一算法:区间覆盖
c++·算法·青少年编程·信息学竞赛
少儿编程乔老师1 年前
NOI1995:石子合并
c++·算法·青少年编程·动态规划·信息学竞赛