F. Array Stabilization (GCD version)
题目链接:https://codeforces.com/contest/1547/problem/F
所用算法:
- 解法一:二分+ST 表(区间 gcd)
- 解法二:思维+筛法筛素因子+stl::set 元素去重
题意:
给定一个 n 个数的数组,可以对这个数组进行若干次操作,每次操作使得这个数组的每个元素 \(a_i\) 变为原来数组中的 \(a_i\) 和 \(a_{(i+1)mod\,n}\) 的最大公因数,即 \(gcd(a_i,a_{i+1})\),问最少需要多少次操作可以使得数组每个元素都相等。
t 次查询,每次给定 n 个数 \(a_0\sim a_{n-1}\),输出最少操作数。(\(2\le{n}\le{2\cdot 10^5},1\le{a_i}\le{10^6},t次查询n的总数量不超过10^5\))
思路:
思路 1:
容易看出进行 x 次操作后,a[i]的值就等于最初始数组的 \(a[i]\sim a[i+x]\) 的 \(gcd\),即区间 \([i,i+x]\) 的区间 \(gcd\),由区间 gcd 我们可以想到 ST 表。
ST 表可以在 \(O(n(\log{w}+\log{n}))\)(\(n\) 为元素个数,\(w\) 为元素值域) 复杂度的预处理之后每次 \(O(\log{w})\) 地查询区间 gcd,这里 \({n}\le{2\cdot 10^5},w\le{10^6}\),因此预处理复杂度最大为 \(O(c*10^6)\)。
分析本题,操作 x 次之后得到的数组中的每个元素即为最初始数组中长度为 x+1 的区间的区间 gcd,于是我们的目标就变为了求使得每个区间的 gcd 相同的最小区间长度,最小区间长度-1 即为答案,由于这个长度的范围在 1~n 之间,而每次判断长度是否合适需要枚举 1~n 每个位置并进行区间 gcd 查询,复杂度为 \(O(n\log{w})\),因此我们需要使用二分来查找这个合适的长度,这样总体复杂度就只会达到 \(O(n\log{w}*\log{n})\),在本题中最大为 \(O(c*10^7)\)
要注意的是本题的数组是首尾相连的,若查询的区间超出了尾部,则需要分成首尾的两个区间分别查询区间 gcd,对这两个区间的 gcd 再求一次 gcd 即可得到要查询区间的区间 gcd。
于是解法 1 即为二分查找答案,并使用 ST 表查询区间 gcd
思路 2:
由 gcd 的性质可知,若经过若干次操作后数组中的 n 个数都相等,这个相等的值一定就是整个区间 \([1,n]\) 的区间 gcd,于是我们可以直接将每个数都除以这个整体的区间 gcd 得到一个新数组,使这两个数组内每个元素相等需要进行的操作数是相同的,而新数组进行若干次操作后最后相等的值一定为 1,因此需要进行的操作数即为新数组内区间 \(gcd>1\) 的最长区间的长度。
于是我们可以先预处理出新数组中每个数的不同素因子的个数,若两个数有相同的素因子,则它们的 \(gcd>1\),然后枚举每个数,再枚举当前数的每个素因子,从 0 开始向左和向右不断扩展区间长度,若相邻数也有当前素因子,就将该数的这个素因子删除,并继续扩展区间长度,保存最长的区间长度即可,这样复杂度即为 O(n*最大素因子个数),由于 \(10^6\) 内的数的素因子个数最多为 8 个(\(2*3*5*7*11*13*17*19=9699690>10^6\)),因此复杂度不超过 O(\(8n\))
要使用筛法(筛出每个数的素因子)以及 stl::set(将每个数的素因子去重,只保留不同素因子)
代码:
代码 1:
//二分+ST表
#include <iostream>
using namespace std;
typedef long long ll;
const int maxn = 2e5 + 5, logmaxn = 21;
int n;
inline int read() //快读
{
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9')
{
if (ch == '-')
f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
{
x = (x << 1) + (x << 3) + (ch - '0'); //(x << 1) + (x << 3)=>x*2+x*8=x*10
ch = getchar();
}
return x * f;
}
//区间GCD ST表
int Log2[maxn], f[maxn][logmaxn]; // 第二维的大小logmaxn根据数据范围决定,不小于log(maxn)
ll gcd(ll a, ll b)
{
return b ? gcd(b, a % b) : a;
}
void getlog2n() //预处理计算log_2^n //O(n)
{
Log2[1] = 0;
for (int i = 2; i < maxn; i++)
Log2[i] = Log2[i / 2] + 1;
}
void getf() //预处理计算f数组 //O(n*(logm+logn))
{
for (int i = 1; i <= n; ++i)
f[i][0] = read();
for (int j = 1; j <= logmaxn - 1; ++j)
for (int i = 1; i + (1 << j) - 1 <= n; ++i)
f[i][j] = gcd(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
}
int query(int l, int r) //每次查询O(logm)
{
int s = Log2[r - l + 1];
return gcd(f[l][s], f[r - (1 << s) + 1][s]);
}
bool check(int len) //判断每个长度为len的区间gcd是否相同 //O(nlogw)
{
int temp = query(1, 1 + len - 1);
for (int i = 2; i <= n; i++) //遍历n个位置+每次求gcd //O(nlogw)
{
int ngcd;
if (i + len - 1 <= n)
ngcd = query(i, i + len - 1);
else
ngcd = gcd(query(i, n), query(1, len - n + i - 1));
if (ngcd != temp)
return 0;
}
return 1;
}
void solve() //二分答案,每次检验O(nlogw),总复杂度O(nlogw*logn)
{ //w<=1e6,n<=2e5 最大复杂度O(c*1e7)
int l = 1, r = n, mid;
while (l < r)
{
mid = (l + r) >> 1;
if (check(mid))
r = mid;
else
l = mid + 1;
}
cout << r - 1 << endl; //操作次数+1=区间长度
}
int main()
{
int t;
cin >> t;
getlog2n(); //预处理获得log_2^i的值
while (t--)
{
n = read();
getf(); //构建ST表
solve();
}
}
代码 2:
//思维解法,除以总体gcd后枚举素因子,使用了欧拉筛以及stl::set
#include <iostream>
#include <cstring>
#include <vector>
#include <set>
#define int long long
using namespace std;
const int maxn = 2e5 + 5, maxnum = 1e6 + 5;
vector<int> primes; //素数表
int mpf[maxnum]; //minimum prime factor
int a[maxn];
set<int> pf[maxn]; //pf[i]表示{a[i]/totgcd}含有的素因子的集合
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
}
void init() //欧拉筛预处理出maxnum以内的素数表以及每个数的最小素因子
{
memset(mpf, 0, sizeof(mpf));
mpf[1] = 1;
for (int i = 2; i < maxnum; i++)
{
if (!mpf[i]) //i为素数
{
mpf[i] = i; //最小素因子为其自身
primes.push_back(i);
}
//遍历素数表,判断 一:i * primes[j] 是否超过 maxnum 二:primes[j] 是否大于i的最小素因子mpf[i]
for (int j = 0; j < primes.size() && i * primes[j] < maxnum && primes[j] <= mpf[i]; j++)
mpf[i * primes[j]] = primes[j]; //i*primes[j]的最小素因子为primes[j]
}
}
void solve()
{
int n, totgcd = 0;
cin >> n;
for (int i = 0; i < n; i++)
{
cin >> a[i];
totgcd = gcd(a[i], totgcd); //计算a1~an,n个数的gcd
}
for (int i = 0; i < n; i++)
{
int temp = a[i];
temp /= totgcd;
while (temp != 1)
{
pf[i].insert(mpf[temp]); //set多次插入相同的数也只会保存一个
temp /= mpf[temp]; //不断除以自己的最小素因子
}
}
int maxlen = 0;
for (int i = 0; i < n; i++)
{
for (int j : pf[i])
{
int len = 1;
int left = (i - 1 + n) % n, right = (i + 1) % n;
while (pf[left].count(j) > 0) //向左
{
pf[left].erase(j);
left = (left - 1 + n) % n;
len++;
}
while (pf[right].count(j) > 0) //向右
{
pf[right].erase(j);
right = (right + 1) % n;
len++;
}
//pf[i].erase(j); //!!调了半天总是运行错误,原因是这里不能erase,因为正在遍历set,这里直接erase会导致越界
maxlen = max(maxlen, len); //保存区间gcd>1的最长区间的长度
}
pf[i].clear(); //遍历完之后再clear
}
cout << maxlen << endl;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
init();
int t;
cin >> t;
while (t--)
solve();
}
总结:
区间 gcd——ST 表(可重复贡献问题)