问题描述
给定一个数组, 每次操作可以选择数组中任意两个相邻的元素 x , y x, y x,y 并将其 中的一个元素替换为 gcd ( x , y ) \operatorname{gcd}(x, y) gcd(x,y), 其中 gcd ( x , y ) \operatorname{gcd}(x, y) gcd(x,y) 表示 x x x 和 y y y 的最大公约数。 请问最少需要多少次操作才能让整个数组只含 1 。
解题思路
关于
g
c
d
gcd
gcd
如果一段子数组的的
g
c
d
gcd
gcd为
1
1
1,那么原数组的
g
c
d
gcd
gcd也一定为
1
1
1。
如果存在一个子数组的
g
c
d
gcd
gcd为
1
1
1,那么这个数组无论再加上任何正整数,
g
c
d
gcd
gcd也永远是
1
1
1,因为
1
1
1和任何数的
g
c
d
gcd
gcd都是
1
1
1。
1、(简单)考虑数组中是否存在
1
1
1,如果数组中存在
1
1
1,依次把全部元素变成
1
1
1,假设
1
1
1的个数为
x
x
x个,那么最终的答案是
n
−
x
n-x
n−x次。
(复杂——原数组中不存在
1
1
1)
2、变出一个
1
1
1,然后使用这个
1
1
1进行平推将数组全部变成
1
1
1。
3、
[
1
,
n
]
[1,n]
[1,n]的
g
c
d
gcd
gcd都不为
1
1
1,那么任何子数组的
g
c
d
gcd
gcd也不可能为
1
1
1,此时为无解。
问题——如何最少次数变出
1
1
1
任务转换——原数组中找到最短的子数组,使得它们的
g
c
d
gcd
gcd为1。
如果
[
l
,
r
]
[l,r]
[l,r]的
g
c
d
gcd
gcd 为
1
1
1
1、那么
[
l
,
r
+
1
]
.
.
.
[
l
,
n
]
[l,r+1]...[l,n]
[l,r+1]...[l,n]这些区间的
g
c
d
gcd
gcd也一定为
1
1
1,
2、而
[
l
,
l
+
1
]
.
.
.
[
l
,
r
−
1
]
[l,l+1]...[l,r-1]
[l,l+1]...[l,r−1]这些区间却并不一定符合条件。
二分法。对于数组中的每个数我们固定为左端点
l
l
l,然后去二分右端点,求出使得
区间
[
l
,
r
]
区间[l,r]
区间[l,r]的
g
c
d
gcd
gcd为
1
1
1的最小的右端点。这样每个数我们都定为左端点去二分它的右端点,所有答案取最小值就能找出
g
c
d
gcd
gcd位
1
1
1的最短区间。
注意:Python语言运行较慢,推荐写成st
表,不推荐写线段树,线段树常数太大。
时间复杂度
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)。
ST算法是基于倍增思想设计的在线算法,记录从每个元素开始的连续长度为
2
k
2^k
2k的区间中元素的GCD。
预处理
类似于动态规划,规定
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示从第
i
i
i 个元素起连续
2
i
2^i
2i个数中的GCD,即记录中的最大公约数。
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]代表的区间
[
i
,
i
+
2
j
−
1
]
[i,i+2^j-1]
[i,i+2j−1]的最大公约数,而区间
[
i
,
i
+
2
j
−
1
]
[i,i+2^j-1]
[i,i+2j−1]上包含偶数个数字,故把
[
i
,
i
+
2
j
−
1
]
[i,i+2^j-1]
[i,i+2j−1]平均分成两段,从
i
i
i 到
i
+
2
j
−
1
−
1
i+2^{j-1}-1
i+2j−1−1为一段,
i
+
2
j
−
1
i+2^{j-1}
i+2j−1到
i
+
2
j
−
1
i+2^j-1
i+2j−1为另一段,长度均为
2
j
−
1
2^{j-1}
2j−1
d
p
[
i
]
[
j
]
=
g
c
d
(
d
p
[
i
]
[
j
−
1
]
,
d
p
[
i
+
2
j
−
1
]
[
j
−
1
]
)
dp[i][j]=gcd(dp[i][j−1],dp[i+2^{j−1}][j−1])
dp[i][j]=gcd(dp[i][j−1],dp[i+2j−1][j−1])
查询
1、不允许重叠,区间的长度按照二进制分成多个
2
2
2的整数倍区间,这些区间是求多次最大公约数能得到答案。
2、允许区间重叠,所有的情况下最多只要两个区间要求区间 ,
[
l
,
r
]
[l,r]
[l,r]中的最大公约数,找到一个数
k
k
k 使得
2
k
<
r
−
l
+
1
2^k <r−l+1
2k<r−l+1,即
k
=
⌊
log
2
(
r
−
l
+
1
)
⌋
k = \lfloor \log_2(r-l+1) \rfloor
k=⌊log2(r−l+1)⌋
可以把这个区间分成两个部分:
[
l
,
l
+
2
k
−
1
]
[l, l+2^k-1]
[l,l+2k−1]和
[
r
−
2
k
+
1
,
r
]
[r-2^k+1, r]
[r−2k+1,r]。
这两个区间恰好是刚刚已经初始化好的,前者对应的是
d
p
[
l
]
[
k
]
dp[l][k]
dp[l][k],后者对应的是
d
p
[
r
−
2
k
+
1
]
[
k
]
dp[r-2^k+1][k]
dp[r−2k+1][k]。
Ac_code
python
# -*- coding: utf-8 -*-
# @Author : BYW-yuwei
# @Software: python3.8.6
from math import gcd
import math
def rmq_init(arr):
arr_len = len(arr)
exp = int(math.log(arr_len, 2))
dp = [[0] * (exp + 1) for _ in range(arr_len + 1)]
for i, a in enumerate(arr):
dp[i + 1][0] = a
for j in range(1, exp + 1):
for start in range(1, arr_len + 1):
if start + (1 << j) - 1 > arr_len:
break
dp[start][j] = gcd(dp[start][j - 1], dp[start + (1 << (j - 1))][j - 1])
return dp
def rmq_ask(dp, left, right):
k = int(math.log(right - left + 1, 2))
return gcd(dp[left][k], dp[right + 1 - (1 << k)][k])
n = int(input())
a = list(map(int, input().split()))
cnt1 = sum(ai == 1 for ai in a)
if cnt1 > 0:
print(n - cnt1)
else:
dp = rmq_init(a)
if rmq_ask(dp, 1, n) != 1:
print(-1)
else:
ans = 10 ** 9
for i in range(1, n):
l, r = i, n
while l < r:
mid = (l + r) >> 1
if rmq_ask(dp, i, mid) == 1:
r = mid
else:
l = mid + 1
if rmq_ask(dp, i, r) == 1:
ans = min(ans, r-i)
print(ans + n-1)