浅谈 Baka's Trick / 不带删尺取 / 对顶栈

1. 算法介绍

1.1 维护普通队列

问题:维护一个队列,支持 pop_frontpush_back,查询队列内所有元素的信息和。保证该信息具有结合律。不保证该信息具有可差分性。

平凡的做法是用线段树或 ST 表维护这种不可差分的信息,然后跑双指针,时间复杂度大部分情况下会比普通双指针多一个 \(\log\)

Baka's Trick / 不带删尺取 / 对顶栈 则通过记录前后缀信息,结合均摊复杂度,在大多数情况下可以将时间复杂度减少一个 \(\log\)

具体而言,算法本质上是维护了一个对顶栈,来模拟队列的操作:

  • 假设有两个栈:\(stk1, stk2\)\(stk1\) 存出队的元素信息,\(stk2\) 存入队的元素信息。
  • 入队时,直接扔到 \(stk2\) 栈顶,并且记录 \(stk2\) 栈内的前缀信息,以方便撤销入栈。
  • 出队时:
    • \(stk1\) 内仍有元素,则直接出队,回退版本。
    • 否则说明 \(stk1\) 内没有元素,将 \(stk2\) 内的元素全部堆进 \(stk1\)
  • 查询时,直接将两个栈的前缀信息合并即可。

因为每个元素最多只会进入一次 \(stk1\),因此时间复杂度 \(O(nB)\)。其中 \(B\) 指将两个信息合并的复杂度。

三指针的写法和对顶栈本质是一样的,个人感觉对顶栈更直观。

1.2 维护双端队列

问题:维护一个双端队列,支持 pop_frontpop_backpush_frontpush_back,查询队列内所有元素的信息和。保证该信息具有结合律。不保证该信息具有可差分性。

和维护普通队列差不多,但是运用了一些均摊技巧。

发现难点在于删除元素的时候要把两个栈里的元素倒过来又倒回去,时间复杂度可能会爆炸。这里有一种暴力重构的方法可以解决这个问题:每次遇到删除的栈为空的时候,暴力重构另一个栈,使得当前的两个栈大小之差不超过 \(1\)

时间复杂度依然是 \(O(nB)\) 的。证明可以考虑势能分析,假设 \(\omega\) 表示当前两个栈大小的绝对值之差,则每次 push 操作最多会使得势能增加 \(1\);而每次删除操作要么使得势能增加 \(1\),要么将势能降到小于等于 \(1\)。而总共只能增加 \(q\) 的势能,所以时间复杂度依然是 \(O(nB)\) 的。

2 例题

2.1 CF1548B Integers Have Friends

先观察朋友团的性质,转化同余的形式为 \(a_i = k_i\times m + b_i\)。发现后面的 \(b_i\) 都必须相同,所以先消掉 \(a_i\) 中的 \(b_i\),然后发现剩下的数都是 \(m\) 的倍数,所以求 \(\gcd\) 判断是否大于 \(1\) 即可。

由此可以想到用差分去抵消相邻两个数中这个 \(b_i\) 的值,然后对所有的差分值求 \(\gcd\),若 \(\gcd > 1\),则是朋友团。

接下来可以直接 ST 表 + 双指针,时间复杂度 \(O(n\log V)\)

但是这题也可以不写 ST 表,用对顶栈做,因为 \(\gcd\) 具有结合律,但不具有可差分性,于是套用对顶栈维护队列的模板即可。时间复杂度 \(O(n\log V)\)

#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi = pair<int, int>;
const int N = 200005;
int n;
ll a[N];
ll gcd(ll a, ll b)
{
    if(b == 0) return a;
    return gcd(b, a % b);
}
ll stk1[N], stk2[N], tp1, tp2, b[N], oristk2[N];
ll getgcd()
{
    return gcd(stk1[tp1], stk2[tp2]);
}
void del()
{
    if(tp1 > 0)
    {
        tp1--;
        return;
    }
    for(int i = tp2; i >= 1; i--)
    {
        ll val = gcd(stk1[tp1], oristk2[i]);
        stk1[++tp1] = val;
    }
    tp2 = 0;
}
int nid = 0;
void insert(ll x)
{
    oristk2[++tp2] = x;
    stk2[tp2] = gcd(x, stk2[tp2 - 1]);
}
void solve()
{
    ll res = 0;
    cin >> n;
    nid++;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        b[i] = abs(a[i] - a[i - 1]);
    }
    tp1 = tp2 = 0;
    for(int i = 2; i <= n; i++)
    {
        if(i == 2)
        {
            insert(b[i]);
            if(b[i] != 1) res = 1;
            continue;
        }
        while(tp1 + tp2 > 0 && gcd(b[i], getgcd()) == 1)
            del();
        if(b[i] == 1) continue;
        insert(b[i]);
        res = max(res, tp1 + tp2);
    }
    cout << res + 1 << "\n";
}
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int t;
    cin >> t;
    while(t--) solve();
    return 0;
}

2.2 LOJ P6515 「雅礼集训 2018 Day10」贪玩蓝月

题意转化为维护一个双端队列,将双端队列里的元素做背包,求体积在模意义下处于 \([l, r]\) 的价值最大值。

显然一个物品与一个背包合并是 \(O(m)\) 的,而合并两个背包是 \(O(m^2)\) 的。所以关键就在于如何快速求出最后合并答案的部分,其余套用对顶栈维护双端队列模板即可。

发现当一个背包选择的体积固定的时候,另一个背包的合法体积在值域上也形成一个连续的区间,且体积改变时,这个区间也只会朝一个方向移动。所以考虑枚举第一个背包的体积,对第二个背包跑单调队列优化枚举过程即可。时间复杂度 \(O(nm)\)

这是一种在线做法;如果需要离线做法可以考虑线段树分治,时间复杂度是 \(O(nm\log n)\) 的。

#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi = pair<int, int>;
const int N = 50005, M = 505;
const ll inf = 0x3f3f3f3f3f3f3f3f;
ll n, m;
struct Backpack{
    ll dp[M], w, v;
    Backpack()
    {
        memset(dp, -0x3f, sizeof(dp));
        dp[0] = 0;
    }
}initb;
vector<Backpack> stk1, stk2;
int tp1, tp2;
Backpack update(Backpack a, ll w, ll v)
{
    Backpack res = a;
    res.w = w, res.v = v;
    for(int i = 0; i < m; i++)
        res.dp[(i + w) % m] = max(res.dp[(i + w) % m], a.dp[i] + v);
    return res;
}
ll f[2 * M], q[2 * M];
ll combine(Backpack x, Backpack y, ll ql, ll qr)
{
    ll res = -1;
    for(int i = 0; i < m; i++) f[i] = f[i + m] = y.dp[i];
    ql += m, qr += m;
    int l = 1, r = 0;
    for(int i = qr; i > ql; i--)
    {
        while(r - l >= 0 && f[q[r]] <= f[i]) r--;
        q[++r] = i;
    }
    for(int i = 0; i < m; i++)
    {
        while(r - l >= 0 && q[l] > qr - i) l++;
        while(r - l >= 0 && f[q[r]] <= f[ql - i]) r--;
        q[++r] = ql - i;
        res = max(res, x.dp[i] + f[q[l]]);
    }
    return res;
}
Backpack g[N];
void rebuild()
{
    int cnt = 0;
    for(int i = tp1; i > 0; i--)
    {
        g[++cnt].w = stk1[tp1].w;
        g[cnt].v = stk1[tp1].v;
    }
    for(int i = 1; i <= tp2; i++)
    {
        g[++cnt].w = stk2[i].w;
        g[cnt].v = stk2[i].v;        
    }
    stk1.clear();
    stk2.clear();
    stk1.push_back(initb);
    stk2.push_back(initb);
    tp1 = (cnt >> 1);
    tp2 = cnt - tp1;
    int cur = 0;
    for(int i = tp1; i >= 1; i--)
    {
        Backpack tmp = update(stk1[cur], g[i].w, g[i].v);
        stk1.push_back(tmp);
        cur++;
    }
    cur = 0;
    for(int i = tp1 + 1; i <= cnt; i++)
    {
        Backpack tmp = update(stk2[cur], g[i].w, g[i].v);
        stk2.push_back(tmp);
        ++cur;
    }
}
int main()
{
    //freopen("sample.in", "r", stdin);
    //freopen("sample.out", "w", stdout);
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int cid;
    cin >> cid >> n >> m;
    stk1.reserve(n + 1);
    stk2.reserve(n + 1);
    stk1.push_back(initb);
    stk2.push_back(initb);
    for(int i = 1; i <= n; i++)
    {
        char s[5];
        cin >> s + 1;
        ll x, y;
        if(s[1] == 'I' && s[2] == 'F')
        {
            cin >> x >> y;
            x %= m;
            Backpack tmp = update(stk1[tp1], x, y);
            tp1++;
            stk1.push_back(tmp);
        }
        else if(s[1] == 'I' && s[2] == 'G')
        {
            cin >> x >> y;
            x %= m;
            Backpack tmp = update(stk2[tp2], x, y);
            tp2++;
            stk2.push_back(tmp);
        }
        else if(s[1] == 'D' && s[2] == 'F')
        {
            if(tp1 == 0) rebuild();
            if(tp1 == 0)
            {
                tp2--;
                stk2.pop_back();
                continue;
            }
            tp1--;
            stk1.pop_back();
        }
        else if(s[1] == 'D' && s[2] == 'G')
        {
            if(tp2 == 0) rebuild();
            if(tp2 == 0)
            {
                tp1--;
                stk1.pop_back();
                continue;
            }
            tp2--;  
            stk2.pop_back();          
        }
        else
        {
            cin >> x >> y;
            cout << combine(stk1[tp1], stk2[tp2], x, y) << "\n";
        }
    }
    return 0;
}

2.3 AT_jag2018summer_day2_d Knapsack And Queries

因为题目保证了每次插入的数都是最大的数,删除的数都是最小的数,可以抽象成一个队列,像上一题那样维护背包即可,只是这个题只需要维护队列,不需要维护双端队列。时间复杂度 \(O(nm)\)

#include <bits/stdc++.h>
#include <cstdint>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi = pair<int, int>;
class Crypto {
public:    
    Crypto() {
        sm = cnt = 0;
        seed();
    }

    int decode(int z) {
        z ^= next();
        z ^= (next() << 8);
        z ^= (next() << 16);
        z ^= (next() << 22);
        return z;
    }

    void query(long long z) {
        const long long B = 425481007;
        const long long MD = 1000000007;
        cnt++;
        sm = ((sm * B % MD + z) % MD + MD) % MD;
        seed();
    }
private: 
    long long sm;
    int cnt;

    uint8_t data[256];
    int I, J;

    void swap_data(int i, int j) {
        uint8_t tmp = data[i];
        data[i] = data[j];
        data[j] = tmp;    
    }

    void seed() {
        uint8_t key[8];
        for (int i = 0; i < 4; i++) {
            key[i] = (sm >> (i * 8));
        }
        for (int i = 0; i < 4; i++) {
            key[i+4] = (cnt >> (i * 8));
        }

        for (int i = 0; i < 256; i++) {
            data[i] = i;
        }
        I = J = 0;

        int j = 0;
        for (int i = 0; i < 256; i++) {
            j = (j + data[i] + key[i%8]) % 256;
            swap_data(i, j);
        }
    }

    uint8_t next() {
        I = (I+1) % 256;
        J = (J + data[I]) % 256;
        swap_data(I, J);
        return data[(data[I] + data[J]) % 256];
    }
};
const int N = 100005, M = 505;
const ll inf = 0x3f3f3f3f3f3f3f3f;
ll n, m;
struct Backpack{
    ll dp[M], w, v;
    Backpack()
    {
        memset(dp, -0x3f, sizeof(dp));
        dp[0] = 0;
    }
}initb;
vector<Backpack> stk1, stk2;
int tp1, tp2;
Backpack update(Backpack a, ll w, ll v)
{
    Backpack res = a;
    res.w = w, res.v = v;
    for(int i = 0; i < m; i++)
        res.dp[(i + w) % m] = max(res.dp[(i + w) % m], a.dp[i] + v);
    return res;
}
ll f[2 * M], q[2 * M];
ll combine(Backpack x, Backpack y, ll ql, ll qr)
{
    ll res = -1;
    for(int i = 0; i < m; i++) f[i] = f[i + m] = y.dp[i];
    ql += m, qr += m;
    int l = 1, r = 0;
    for(int i = qr; i > ql; i--)
    {
        while(r - l >= 0 && f[q[r]] <= f[i]) r--;
        q[++r] = i;
    }
    for(int i = 0; i < m; i++)
    {
        while(r - l >= 0 && q[l] > qr - i) l++;
        while(r - l >= 0 && f[q[r]] <= f[ql - i]) r--;
        q[++r] = ql - i;
        res = max(res, x.dp[i] + f[q[l]]);
    }
    return res;
}
Backpack g[N];
void rebuild()
{
    int cnt = 0;
    for(int i = tp1; i > 0; i--)
    {
        g[++cnt].w = stk1[tp1].w;
        g[cnt].v = stk1[tp1].v;
    }
    for(int i = 1; i <= tp2; i++)
    {
        g[++cnt].w = stk2[i].w;
        g[cnt].v = stk2[i].v;        
    }
    stk1.clear();
    stk2.clear();
    stk1.push_back(initb);
    stk2.push_back(initb);
    tp1 = (cnt >> 1);
    tp2 = cnt - tp1;
    int cur = 0;
    for(int i = tp1; i >= 1; i--)
    {
        Backpack tmp = update(stk1[cur], g[i].w, g[i].v);
        stk1.push_back(tmp);
        cur++;
    }
    cur = 0;
    for(int i = tp1 + 1; i <= cnt; i++)
    {
        Backpack tmp = update(stk2[cur], g[i].w, g[i].v);
        stk2.push_back(tmp);
        ++cur;
    }
}
int main()
{
    //freopen("sample.in", "r", stdin);
    //freopen("sample.out", "w", stdout);
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin >> m >> n;
    stk1.reserve(n + 1);
    stk2.reserve(n + 1);
    stk1.push_back(initb);
    stk2.push_back(initb);
    Crypto rndc;
    for(int i = 1; i <= n; i++)
    {
        ll op, x, y, xx, yy;
        cin >> op >> x >> y >> xx >> yy;
        op = rndc.decode(op);
        x = rndc.decode(x);
        y = rndc.decode(y);
        xx = rndc.decode(xx);
        yy = rndc.decode(yy);    
        ll ans = -1;  
        if(op == 1)
        {
            x %= m;
            Backpack tmp = update(stk2[tp2], x, y);
            tp2++;
            stk2.push_back(tmp);            
            ans = combine(stk1[tp1], stk2[tp2], xx, yy);
        }  
        else if(op == 2)
        {
            if(tp1 == 0) rebuild();
            if(tp1 == 0)
            {
                tp2--;
                stk2.pop_back();
            }
            else
            {
                tp1--;
                stk1.pop_back();
            }
            ans = combine(stk1[tp1], stk2[tp2], xx, yy);
        }
        rndc.query(ans);
        cout << ans << "\n";
    }
    return 0;
}

2.5 UOJ P693 【UR #23】地铁规划

2.6 P6684 [BalticOI 2020] 小丑 (Day1)

posted @ 2025-10-02 10:46  KS_Fszha  阅读(24)  评论(0)    收藏  举报