特殊技巧练习

CF925E

树上颜色不好 \(\log\) 处理的时候考虑分块。具体到这题用了一个重链 dfs 序分块方便维护,因为到根都是连续的一些段。

CF1418G

拆限制好题。

考虑枚举区间右端点 \(r\),统计有多少合法的 \(l\)

接下来将问题的限制拆分为两部分:

  • 区间 \([l,r]\) 内每个数出现的次数是 \(3\) 的倍数次。
  • 区间 \([l,r]\) 每个数出现的次数不超过 \(3\) 个。

先考虑如何统计满足第一个限制的区间数量。开一个桶记录每个数出现的数量,对 \(3\) 取模,容易得到一个区间 \([l,r]\) 合法当且仅当 \(r\) 对应的桶与 \(l-1\) 对应的桶相同。无法 \(\mathcal O(n)\) 判断两个桶是否相同,可以使用哈希解决。

再考虑第二个限制。只需要使用双指针,在右指针扫到 \(i\) 的时候,不停将左指针向右移动并减去这个桶的出现次数,直到 \(a_i\) 的出现次数小于等于 \(3\) 为止。此时再统计答案,两个限制都可以满足。

在代码实现上,不需要繁琐的字符串哈希(难写而且会被卡单模数),只需要对每个值赋一个 \(10^{18}\) 级别的随机数,桶的哈希值即为每个数出现的次数乘对应的随机数。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define R(i, l, r) for (int i = (l); i <= (r); ++i)
const int N = 5e5 + 5;
const ll P = 1e18;
int n, a[N];
ll rdm[N], ans;
int cnt[N];
__int128 hsh[N]; 
map <__int128, int> mp;
signed main()
{
    cin >> n;
    mt19937_64 rnd(time(0));
    R(i, 1, n) rdm[i] = rnd() % P + 1;
    R(i, 1, n) 
    {
        hsh[i] = hsh[i - 1];
        cin >> a[i];
        hsh[i] -= 1ll * cnt[a[i]] * rdm[a[i]];
        ++cnt[a[i]]; cnt[a[i]] %= 3;
        hsh[i] += 1ll * cnt[a[i]] * rdm[a[i]];
    }
    mp[0] = 1;
    memset(cnt, 0, sizeof cnt);
    for (int i = 1, j = 0; i <= n; ++i)
    {
        ++cnt[a[i]];
        while (cnt[a[i]] > 3) --cnt[a[j]], (j ? --mp[hsh[j - 1]] : 1), ++j;
        ans += mp[hsh[i]];
        ++mp[hsh[i]];
    }
    cout << ans << '\n';
    return 0;
}

CF1673F

格雷码优化构造。

首先我们不妨在一维的一根线上思考这个问题。考虑能否直接表示出来小偷在哪里,我们就可以表示出来道路价值为 \(i\oplus (i+1)\)\(i\) 是你变化的下标。

变成二维就是这个数的前 \(5\) 位表示行,后 \(5\) 位表示列。

但是这样道路价值和太大了,考虑优化这个价值。我们发现代价太大的原因是 \(i\)\((i+1)\) 中间二进制数相差的数可能比较多,所以考虑用格雷码这种相邻两个数只有一个数不同的二进制集来表示。

格雷码性质:

  • \(𝑘\) 位二进制数的格雷码序列可以当作 \(𝑘\) 维空间中的一个超立方体(二维里的正方形,一维里的单位向量)顶点的哈密尔顿回路,其中格雷码的每一位代表一个维度的坐标。
  • 格雷码被用于最小化数字模拟转换器(比如传感器)的信号传输中出现的错误,因为它每次只改变一个位。

然后我们还要优化下,我们发现不要前后行列割裂开而是交错放置行列的格雷码就可以了,因为格雷码低位先有 \(1\)

格雷码生成代码:

int g(int n) { return n ^ (n >> 1); }

格雷码还原原数代码:

int rev_g(int g) {
  int n = 0;
  for (; g; g >>= 1) n ^= g;
  return n;
}

P11458

不会,但是知道了 \(|A\and B|=|A|+|B|-|A\or B|\),不太可能考到高位前缀和吧。

P8264

美难题

套路的,先进行分块。以下我们默认块长为 \(B\)

考虑对每一个块预处理每一个数经过这个块后的数值变化量,直接暴力枚举复杂度显然错完了。但是有一个比较聪明的暴力是,我们发现一个块内的数可以把一个值域区间劈成两半,其中这两半内的数的数值变化量一样,可以继续递归处理,这样的话每一个块处理的复杂度就是 \(O(2^B)\),加上查询的总复杂度是 \(O(\frac{n^2+n2^B}{B})\),当 \(B=\log n\) 时,复杂度为 \(O(\frac{n^2}{\log n})\),看着能过的样子,但实测最高只有九十二分。

我们接着刚才的思路想,发现劈成两半时,两边的数会有重复,具体来说,对于一段值域区间 \([l,r]\),将其从 \(x\) 处劈开,不妨令 \(2x<l+r\),那么 \(\forall a\in[l,x)\),在进行完这次劈开操作后,它的结果会与 \(2x-a\) 完全相同,于是可以将这些结果重复的数用并查集并起来,省流一下也就是将短的那一边并到长的那一边去,这样递归只要往一边走就行了,查询的时候询问一下当前值对应的根节点最后的值就就行了。

由于每个数最多被合并一次,所以时间复杂度是 \(O(\frac{n^2}{B}+nB)\) 的,取 \(B=\sqrt{n}\) 时,复杂度平衡为 \(O(n\sqrt{n})\),可以通过。

#include<bits/stdc++.h>
using namespace std;
int a[100010],ks[100010];
struct node{
    int l,r,fl,dir;
    int fa[100010];
    inline int find(int x){
        if(fa[x]==x)return x;
        return fa[x]=find(fa[x]);
    }
}fk[120];
int ask(int l,int r,int v){
    int ll=ks[l],rr=ks[r];
    if(ll==rr){
        for(int i=l;i<=r;i++)v=abs(v-a[i]);
        return v;
    }
    for(int i=l;i<=fk[ll].r;i++)v=abs(v-a[i]);
    for(int i=ll+1;i<rr;i++){
        v=fk[i].fl*fk[i].find(v)+fk[i].dir;
    }
    for(int i=fk[rr].l;i<=r;i++)v=abs(v-a[i]);
    return v;
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>a[i];
    int len=900,tot=0;
    for(int i=1;i<=n;i++){
        ks[i]=(i-1)/len+1;
        if((i-1)%len==0){
            fk[++tot].l=i;
        }
        if(i%len==0){
            fk[tot].r=i;
        }
    }
    fk[tot].r=n;
    for(int i=1;i<=tot;i++){
        int l=0,r=1e5,fl=1,dir=0;
        for(int j=l;j<=r;j++)fk[i].fa[j]=j;
        for(int j=fk[i].l;j<=fk[i].r;j++){
            if(fl==1){
                if(r+dir<a[j]){
                    fl=-fl,dir=a[j]-dir;
                }
                else if(l+dir>a[j]){
                    dir-=a[j];
                }
                else{
                    if(r+dir-a[j]<a[j]-l-dir){
                        for(int k=r+dir;k>a[j];k--){
                            int fx=fk[i].find(k-dir),fy=fk[i].find(2*a[j]-k-dir);
                            if(fx!=fy)fk[i].fa[fx]=fy;
                        }
                        r=a[j]-dir;
                        fl=-fl,dir=a[j]-dir;
                    }
                    else{
                        for(int k=l+dir;k<a[j];k++){
                            int fx=fk[i].find(k-dir),fy=fk[i].find(2*a[j]-k-dir);
                            if(fx!=fy)fk[i].fa[fx]=fy;
                        }
                        l=a[j]-dir;
                        dir-=a[j];
                    }
                }
            }
            else{
                if(-l+dir<a[j]){
                    fl=-fl,dir=a[j]-dir;
                }
                else if(-r+dir>a[j]){
                    dir-=a[j];
                }
                else{
                    if(-l+dir-a[j]<a[j]+r-dir){
                        for(int k=-l+dir;k>a[j];k--){
                            int fx=fk[i].find(dir-k),fy=fk[i].find(dir-2*a[j]+k);
                            if(fx!=fy)fk[i].fa[fx]=fy;
                        }
                        l=dir-a[j];
                        fl=-fl,dir=a[j]-dir;
                    }
                    else{
                        for(int k=-r+dir;k<a[j];k++){
                            int fx=fk[i].find(dir-k),fy=fk[i].find(dir-2*a[j]+k);
                            if(fx!=fy)fk[i].fa[fx]=fy;
                        }
                        r=dir-a[j];
                        dir-=a[j];
                    }
                }
            }
        }
        fk[i].fl=fl,fk[i].dir=dir;
    }
    int lst=0;
    while(m--){
        int l,r,v;
        cin>>l>>r>>v;
        l^=lst,r^=lst,v^=lst;
        cout<<(lst=ask(l,r,v))<<"\n";
    }
    return 0;
}

CF1253F

推式子题

题意

\(n\) 个点,\(m\) 条边,\(k\) 个充电站,\(Q\) 次询问,每次询问给出两个点 \(p_1\) , \(p_2\) ,两个点一定在充电站上,问从 \(p_1\)\(p_2\) 的电池容量最少需要多少

题解

考虑如何求出任意一个点到离它最近的充电站的距离?

有一个很巧妙的处理方法是,我们可以建一个超级源点,连向所有充电站,权值设为 \(0\),然后在新图上跑一个最短路,就能求出所有点到离它最近的充电站得距离 \(dis\) 了。

然后考虑经过一条边的电池容量最小是多少。比如说一条边从 \(i\) 连向 \(j\)。那么我们会从距离 \(i\) 最近的充电站走向 \(i\),然后经过 \(i\to j\) 这条边,最后走到距离 \(j\) 最近的充电站。所以电池容量 \(c\) 满足 \(c\ge dis_i+w_{i,j}+dis_j\)

可以证明这个东西是对的。比如你现在求出了一条路径,你考虑你要从一个充电站走向另一个充电站,且中间没有其他充电站,最大的部分会在中间出现,而且一定会出现。这个部分代表了从一个充电站走向不同充电站,由此不断推出是可行的。

考虑用这个东西作为新的点权,然后发现新的问题形如从 \(p_1\)\(p_2\) 经过的路径上最大的边最小。所以用 kruscal 重构树维护就好了。

AT_abc250_h

推式子和上面一样,但是之后有更简单的做法,考虑按照边权从小到大排序,询问也离线下来,用并查集处理就好了。

posted @ 2025-11-19 15:27  NeeDna  阅读(3)  评论(0)    收藏  举报