浅谈 Baka's Trick / 不带删尺取 / 对顶栈
1. 算法介绍
1.1 维护普通队列
问题:维护一个队列,支持 pop_front
和 push_back
,查询队列内所有元素的信息和。保证该信息具有结合律。不保证该信息具有可差分性。
平凡的做法是用线段树或 ST 表维护这种不可差分的信息,然后跑双指针,时间复杂度大部分情况下会比普通双指针多一个 \(\log\)。
而 Baka's Trick / 不带删尺取 / 对顶栈 则通过记录前后缀信息,结合均摊复杂度,在大多数情况下可以将时间复杂度减少一个 \(\log\)。
具体而言,算法本质上是维护了一个对顶栈,来模拟队列的操作:
- 假设有两个栈:\(stk1, stk2\),\(stk1\) 存出队的元素信息,\(stk2\) 存入队的元素信息。
- 入队时,直接扔到 \(stk2\) 栈顶,并且记录 \(stk2\) 栈内的前缀信息,以方便撤销入栈。
- 出队时:
- 若 \(stk1\) 内仍有元素,则直接出队,回退版本。
- 否则说明 \(stk1\) 内没有元素,将 \(stk2\) 内的元素全部堆进 \(stk1\)。
- 查询时,直接将两个栈的前缀信息合并即可。
因为每个元素最多只会进入一次 \(stk1\),因此时间复杂度 \(O(nB)\)。其中 \(B\) 指将两个信息合并的复杂度。
三指针的写法和对顶栈本质是一样的,个人感觉对顶栈更直观。
1.2 维护双端队列
问题:维护一个双端队列,支持 pop_front
、pop_back
、push_front
、push_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;
}