二分+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\)

\[\max_{i=l}^r a_i=\min_{i=l}^r b_i \]

  • \(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)\) 即可。

posted @ 2022-05-29 19:30  OIer某罗  阅读(83)  评论(0编辑  收藏  举报