二分+ST表
https://www.luogu.com.cn/problem/solution/P7009
题意:给定一个序列,有 \(T\) 组询问,每次给出 \(n\) 个数 \(a_i\)。
你需要找到这个数组的一个子序列(要求编号连续),使得该序列中所有数的最大公约数和序列长度的乘积最大,并输出这个最大值。
首先,\(\gcd\) 满足可重复合并性质,可以 \(ST\) 表做到不修改的 \(O(1)\) 查询。
然后,前缀 \(\gcd\) 有单调不增的性质,并且有:
\(\mathbf{长度为~n~的不同前缀的 ~\gcd~ 一共有~ \log n~ 种。}\)
对于一定的 \(\gcd\) 的值,肯定是区间长度越大越好,那么我们可以二分查找最右边的端点并更新答案。
因此我们可以枚举左端点,第一次查找 \(\gcd\) 就是其本身的,然后每次二分更新答案,并且将寻找的 \(\gcd\) 改成第一个不满足的 \(\gcd\) 值。
时间复杂度,\(O(n) \times O(\log n) \times O(\log n)\)。
#include<bits/stdc++.h>
using namespace std;
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
int n;
ll a[100010];
ll st[100010][25];
ll query(int x, int y) {
int xx = log2(y - x + 1);
return __gcd(st[x][xx], st[y - (1 << xx) + 1][xx]);
}
int main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
int t; cin >> t;
while(t--) {
cin >> n;
ll ans = 0;
f(i, 1, n) {
cin >> a[i];
st[i][0] = a[i];
}
f(i, 1, 20) {
f(j, 1, n) {
int len = (1 << i);
if(j + len - 1 > n) break;
st[j][i] = __gcd(st[j][i - 1], st[j + (1 << (i - 1))][i - 1]);
}
}
f(l, 1, n) {
ll gcd = a[l], bound = query(l, n), rl = l;
while(gcd > bound) {
int rr = n;
while(rl < rr) {
int mid = (rl + rr + 1) >> 1;
if(query(l, mid) == gcd) rl = mid;
else rr = mid - 1;
}
ans = max(ans, gcd * (rl - l + 1));
gcd = query(l, rl + 1);
rl = rl + 1;
}
ans = max(ans, bound * (n - l + 1));
}
cout << ans << endl;
}
return 0;
}
注意:这个 \(rl\) 一定不能清到 \(l\),否则重复寻找而且慢,还容易挂。
https://codeforces.com/contest/475/problem/D
题意:
给出一个长度为\(n(1<=n<=10^{5})\) 的序列和\(q(1<=q<=3*10^{5})\) 个询问,每个询问输出一行,询问\(gcd(a_l,a_{l+1},...,a_r)=x\) 的\((i,j)\) 的对数.
思路是刚才的变形。只需要预处理即可,一共不会超过 \(n\log n\) 个 \(\gcd\) 的值。
#include<bits/stdc++.h>
using namespace std;
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
int n;
ll a[100010];
ll st[100010][25];
ll query(int x, int y) {
int xx = log2(y - x + 1);
return __gcd(st[x][xx], st[y - (1 << xx) + 1][xx]);
}
map<ll, ll> num;
int main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
cin >> n;
f(i, 1, n) {
cin >> a[i];
st[i][0] = a[i];
}
f(i, 1, 20) {
f(j, 1, n) {
int len = (1 << i);
if(j + len - 1 > n) break;
st[j][i] = __gcd(st[j][i - 1], st[j + (1 << (i - 1))][i - 1]);
}
}
f(l, 1, n) {
ll gcd = a[l], bound = query(l, n), lastrl = l, rl = l;
while(gcd > bound) {
int rr = n;
while(rl < rr) {
int mid = (rl + rr + 1) >> 1;
if(query(l, mid) == gcd) rl = mid;
else rr = mid - 1;
}
if(!num.count(gcd)) num[gcd] = 0;
num[gcd] += (rl - lastrl + 1);
gcd = query(l, rl + 1);
rl = rl + 1; lastrl = rl;
}
if(!num.count(bound)) num[bound] = 0;
num[bound] += (n - lastrl + 1);
}
int q; cin >> q;
while(q--) {
ll x; cin >> x;
if(!num.count(x)) cout << 0 << endl;
else cout << num[x] << endl;
}
return 0;
}
区间 \(\min,\max\) 同样可以使用 ST 表维护。
比如下面这一题:
https://www.luogu.com.cn/problem/CF689D
题意:
- 给定序列 \(a\) 和序列 \(b\),长度均为 \(n\)。问有多少组 \((l,r)\),满足 \(1\le l\le r\le n\) 且
- \(1\le n\le 2\times 10^5\),\(|a_i|,|b_i|\le 10^9\)。
注意到,左端点固定的情况下,\(\max\) 单调不降,\(\min\) 单调不升,于是如果存在 \(r\) 使得满足题意,那么在 \(l~r-1\) 一定是 \(min >= max\),在 \(r+1, n\) 一定是 \(max >= min\)。于是满足题意的 \(r\) 一定是一段数字,分别二分查找左右端点即可。
#include<bits/stdc++.h>
using namespace std;
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
int n;
ll a[200010], b[200010];
ll mn[200010][25], mx[200010][25];
ll qmn(int x, int y) {
int xx = log2(y - x + 1);
return min(mn[x][xx], mn[y - (1 << xx) + 1][xx]);
}
ll qmx(int x, int y) {
int xx = log2(y - x + 1);
return max(mx[x][xx], mx[y - (1 << xx) + 1][xx]);
}
int main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
cin >> n;
f(i, 1, n) {
cin >> a[i];
mx[i][0] = a[i];
}
f(i, 1, n) {
cin >> b[i];
mn[i][0] = b[i];
}
f(i, 1, 20) {
f(j, 1, n) {
int len = (1 << i);
if(j + len - 1 > n) break;
mn[j][i] = min(mn[j][i - 1], mn[j + (1 << (i - 1))][i - 1]);
mx[j][i] = max(mx[j][i - 1], mx[j + (1 << (i - 1))][i - 1]);
}
}
ll ans = 0;
f(i, 1, n) {
if(a[i] > b[i]) continue;
if(qmx(i, n) < qmn(i, n)) continue;
int ll = i, lr = n;
while(ll < lr)
{
int mid = (ll + lr) >> 1;
if(qmx(i, mid) < qmn(i, mid)) ll = mid + 1;
else lr = mid;
}
int rl = i, rr = n;
while(rl < rr) {
int mid = (rl + rr + 1) >> 1;
if(qmx(i, mid) > qmn(i, mid)) rr = mid - 1;
else rl = mid;
}
if(ll > rl) continue;
else ans += rl - ll + 1;
}
cout << ans << endl;
return 0;
}
https://www.luogu.com.cn/problem/CF359D
题意:
$ Simon $ 有一个长度为 $ N $ 的正整数数列 $ a_1 , a_2 , \cdots , a_n $ ,现在想找到这个数列中最长的一个区间,满足区间中有一个数 $ x $ 可以整除区间中任意数。
第一行输出两个正整数 \(cnt~,~len\),表示满足要求的最长区间的个数与长度。
第二行输出 \(cnt\) 个升序排列的正整数,表示所有满足要求的最长区间的左端点。
这里,区间的长度定义为右端点减左端点。
只需要判断区间 \(\gcd\) 是不是等于区间 \(\min\) 即可。(这个数 \(x\) 一定是最小值)
\(\gcd\) 和 \(\min\) 都是单减,没有单调性。从哪儿入手呢?
如果一个区间满足题意,那么它包含 \(\min\) 的那个子区间一定也满足题意,比如:
\(6 9 3\) 满足题意,那么 \(9 3\) 一定满足题意。
因此区间的长度有单调性。
我们可以二分区间长度,然后对于每一个长度, \(O(n)\) 枚举左端点,并判断是否存在一个 \(l,r\) 满足 \(\gcd(l,r)==\min(l, r)\) 即可。