P8792 [藍橋杯 2022 國 A] 最大公約數

ltign發表於2024-05-27

P8792 [藍橋杯 2022 國 A] 最大公約數

一、問題簡析

st表 + 二分

思路

要使數列都變成 \(1\),首先數列中要有 \(1\)。因為題目要求是用兩個數的 \(gcd\) 代替其中一個數,所以我們要找到一個區間 \([L, R]\),該區間的 \(gcd\) 等於 \(1\)

證:

\[\begin{split} a_{L+1} &= gcd(a_L, a_{L+1}) \neq 1 \\ a_{L+2} &= gcd(a_{L+1}, a_{L+2}) \neq 1 \\ &...\\ a_{R-1} &= gcd(a_{R-2}, a_{R-1}) \neq 1\\ a_{R} &= gcd(a_{R - 1}, a_R) = 1 \\ \\ \therefore gcd(&a_L,a_{L+1},...,a_R)=1 \end{split} \]

因此,我們需要查詢區間 \(gcd\)。因為不需要區間修改,所以選擇 \(st\) 表。

設數列長度為 \(n\),數列中 \(1\) 的個數為 \(cnt\),使區間 \(gcd=1\) 的最小區間長度為 \(len\),則答案為

if cnt > 0
    ans = n - cnt // 非1的個數,就是操作次數
else if cnt == 0
    if not exit such len
        ans = -1 // 不能使數列都為1
    else if exit such len
        ans = len + n - 2 // 需要len-1次操作獲得第一個1,此時數列中非1元素個數為n-1

\(st\)

按照模板修改即可 st表

二分

本題另一個問題就是找到上述的 \(len\)。我們可以對數列長度 \(n\) 進行二分,找到最小的長度 \(len\),使得數列中存在長度為 \(len\) 的區間 \(gcd=1\)

// 二分判斷函式,長度為x的區間是否gcd為1 
bool check(int x)
{
	for (int i = 1; i + x - 1 <= n; ++i)
	{
		int j = i + x - 1;
		if (query(i, j) == 1)    return true;
	}
	return false;
}

// 找到len
void solve(void)
{
    int L = 1, R = n, len;
	while (L <= R)
	{
		int M = (L + R) >> 1;
		if (check(M))
		{
			len = M;
			R = M - 1;
		}
		else
			L = M + 1;
	}
}

Code

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;

ll quickin(void)
{
	ll ret = 0;
	bool flag = false;
	char ch = getchar();
	while (ch < '0' || ch > '9')
	{
		if (ch == '-')    flag = true;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9' && ch != EOF)
	{
		ret = ret * 10 + ch - '0';
		ch = getchar();
	}
	if (flag)    ret = -ret;
	return ret;
}

const int MAX = 1e5 + 5;
int st[MAX][30], lg2[MAX], pw2[30], n;
int cnt = 0;    // 記錄數列中1的個數

int gcd(int a, int b)
{
	if (b == 0)    return a;
	return gcd(b, a % b);
}

void init(void)
{
	lg2[1] = 0;
	for (int i = 2; i <= n; ++i)    lg2[i] = lg2[i >> 1] + 1;
	pw2[0] = 1;
	for (int i = 1; i < 30; ++i)    pw2[i] = pw2[i - 1] << 1;
	
	for (int i = 1; i <= n; ++i)
	{
		st[i][0] = quickin();
		if (st[i][0] == 1)    ++cnt; // 統計1的個數 
	}
	for (int j = 1; j <= lg2[n]; ++j)
		for (int i = 1; i + pw2[j] - 1 <= n; ++i)
			st[i][j] = gcd(st[i][j - 1], st[i + pw2[j - 1]][j - 1]);
}

int query(int L, int R)
{
	int k = lg2[R - L + 1];
	return gcd(st[L][k], st[R - pw2[k] + 1][k]);
}

// 二分判斷函式,長度為x的區間是否gcd為1 
bool check(int x)
{
	for (int i = 1; i + x - 1 <= n; ++i)
	{
		int j = i + x - 1;
		if (query(i, j) == 1)    return true;
	}
	return false;
}

int main()
{
	#ifdef LOCAL
	freopen("test.in", "r", stdin);
	#endif
	
	n = quickin();
	
	init();
	
	if (cnt > 0)
	{
		printf("%d\n", n - cnt);
		return 0;
	}
	
	if (query(1, n) > 1)    printf("-1\n");
	else
	{
		// 對區間長度n二分,找到最小的區間長度,使gcd==1 
		int L = 1, R = n, ans = 1e8;
		while (L <= R)
		{
			int M = (L + R) >> 1;
			if (check(M))
			{
				ans = M;
				R = M - 1;
			}
			else
				L = M + 1;
		}
		
		printf("%d\n", ans + n - 2);
	}
	
	return 0;
}