十三届蓝桥杯研究生组国赛-最大公约数
1、问题描述
问题描述
给定一个数组, 每次操作可以选择数组中任意两个相邻的元素 x , y x,y x,y并将其 中的一个元素替换为 g c d ( x , y ) gcd(x,y) gcd(x,y), 其中 表 g c d ( x , y ) gcd(x,y) gcd(x,y)示 x 和 y 的最大公约数。 请问最少需要多少次操作才能让整个数组只含 1 。
输入格式
输入的第一行包含一个整数 n, 表示数组长度。
第二行包含 n 个整数 a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an, 相邻两个整数之间用一个空格分隔。
输出格式
输出一行包含一个整数, 表示最少操作次数。如果无论怎么操作都无法满足要求, 输出−1 。
样例输入
3
4 6 9
样例输出
4
评测用例规模与约定
对于 30% 的评测用例, n ≤ 500 , a i ≤ 1000 n \le 500,a_i \le 1000 n≤500,ai≤1000;
对于 50% 的评测用例, n ≤ 5000 , a i ≤ 1 0 6 n \le 5000,a_i \le 10^6 n≤5000,ai≤106;
对于所有评测用例, 1 ≤ n ≤ 100000 , 1 ≤ a i ≤ 1 0 9 1\le n \le 100000,1 \le a_i \le 10^9 1≤n≤100000,1≤ai≤109 。
运行限制
- 最大运行时间:3s
- 最大运行内存: 512M
2、解题思路
对样例输入和输出的解释如下:
[
4
,
6
,
9
]
→
[
4
,
3
,
9
]
→
[
1
,
3
,
9
]
→
[
1
,
1
,
9
]
→
[
1
,
1
,
1
]
[
4
,
6
,
9
]
→
[
4
,
2
,
9
]
→
[
4
,
2
,
1
]
→
[
4
,
1
,
1
]
→
[
1
,
1
,
1
]
[4,6,9]\to[4,3,9]\to[1,3,9]\to[1,1,9]\to[1,1,1]\\ [4,6,9]\to[4,2,9]\to[4,2,1]\to[4,1,1]\to[1,1,1]
[4,6,9]→[4,3,9]→[1,3,9]→[1,1,9]→[1,1,1][4,6,9]→[4,2,9]→[4,2,1]→[4,1,1]→[1,1,1]
上面两种方案都是可行的,所以最少需要4次gcd操作才能将所有数字都变成1。
题目要的是最少需要多少次GCD操作才能让数组值全为1.
我们首先应该考虑数组中是否存在1,如果数组中存在1,那直接让整个1和其他数字都进行gcd,假设1的个数为x
,那么最终答案为n-x
次。
如果数组中不存在1,那我我们应该用过gcd操作算出一个1,然后利用这个1和gcd让其他值都变成1
如果一个子数组的gcd为1,那么原数组的gcd也一定为1。因为如果存在一个数组的gcd为1,那么整个数组无论再加上任何正整数,gcd也永远是1,因为1和任何数的gcd都是1。
由于要求的是最少次数,所以在没有1的情况下,我们要使用最少的操作次数获得1,其实就是我们在数组中找到最短的子数组,使得它们的gcd结果为1,涉及到了查询区间gcd整个操作。两种方案,一种暴力,但是会超时,另一种是线段树。
如何寻找最短的子数组满足区间gcd为1呢?我们考虑使用二分法,对于数组中的每个数我们都可以固定为区间的左端点 l e f t left left,然后去二分它的右端点,求出使得区间 [ l e f t , r i g h t ] [left,right] [left,right]的gcd为1的最小的右端点。
既然二分就需要满足二段性,根据上面的描述,我们可以知道,如果 [ l e f t , r i g h t ] [left,right] [left,right]的gcd为1,那么 [ l e f t , r i g h t + 1 ] , . . . [ l e f t , n ] [left,right+1],...[left,n] [left,right+1],...[left,n]这些区间的gcd也一定为1,而 [ l e f t , l e f t + 1 ] , . . . , [ l e f t , r i g h t − 1 ] [left,left+1],...,[left,right-1] [left,left+1],...,[left,right−1]这些区间却不一定符合条件。这样每个数我们都定位左端点去二分它的右端点,答案取最小值就能找到gcd为1的最短区间。
判断无解的情况,如果整个数组的gcd都不为1,那么任何子数组的gcd也不可能为1,此时无解。
有关线段树的基本操作请看我这篇博客:线段树入门
2.1 解法一:暴力查询区间gcd(75%)
这种想法是对的,就是在查询区间gcd这里超时了,我在官网测试的时候通过了75%的数据。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
//75%
public class Main1 {
public static int MAX_N = 1000010;
public static int[] a = new int[MAX_N];
public static StreamTokenizer st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
public static void main(String[] args) throws IOException {
int n = nextInt();
int tmp = 0;
for (int i = 1; i <= n; i++) {
a[i] = nextInt();
if (a[i] == 1) {
tmp++; //统计1的个数
}
}
if (tmp > 0) { //有1可以直接返回结果
System.out.println(n - tmp);
return;
}
tmp = n;
//判断是否无解
int ans = a[1];
for (int i = 2; i <= n; i++) { //判断区间所有数字的gcd是否为1
ans = gcd(ans, a[i]);
}
if (ans != 1) { //无解
System.out.println(-1);
return;
}
for (int i = 1; i <= n; i++) { //枚举左端点
int d = a[i];
for (int j = i + 1; j <= n; j++) {//枚举右端点
d = gcd(d, a[j]);
if (d == 1) {
//区间长度为j-i+1,但是我们将所有数字变成1的操作次数是j-i
tmp = Math.min(tmp, j - i);
break;
}
}
}
System.out.println(n - 1 + tmp);
}
public static int gcd(int a, int b) {
if (b == 0) {
return a;
}
int min = Math.min(a, b);
int max = Math.max(a, b);
return gcd(min, max % min);
}
public static int nextInt() throws IOException {
st.nextToken();
return (int) st.nval;
}
}
2.2 解法二:线段树+二分法(AC)
package 十三届蓝桥杯国赛.最大公约数;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
//AC:线段树+二分
public class Main {
public static StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
public static int MAXN=1000010;
public static int[] arr=new int[MAXN];
public static int[] tree=new int[MAXN<<2];//4n
public static void main(String[] args)throws IOException {
int n=nextInt();
int tmp=0;
for (int i = 0; i <n; i++) {
arr[i]=nextInt();
if(arr[i]==1){
tmp++;
}
}
if(tmp>0){ //有1的话可以直接得出结果
System.out.println(n-tmp);
return;
}else{
tmp=n;
}
//构造线段树
buildTree(0,0,n-1);
if(queryTree(0,0,n-1,0,n-1)!=1){
System.out.println(-1);
return;
}
for(int i=0;i<n;i++){ //枚举左端点
int left=i+1;
int right=n-1;
int mid;
while(left<right){ //二分法枚举出右端点
mid=(left+right)>>1;
if(queryTree(0,0,n-1,i,mid)==1){
right=mid;
}else{
left=mid+1;
}
}
if(queryTree(0,0,n-1,i,right)==1){
//tmp最终保留的是满足有解的最小区间[i-right]
//区间长度为right-i+1,但是我们要的是全部执行gcd变成1的操作次数,所以我right-i
tmp=Math.min(tmp,right-i);
}
}
//tmp表示变一个1出来的最少操作次数,然后再加上将剩下的n-1个数变成1就是最后的结果了
System.out.println(n-1+tmp);
}
//构建线段树
public static void buildTree(int node,int start,int end){
if(start==end){
tree[node]=arr[start];
}else{
int mid=(start+end)>>1;
int leftNode=2*node+1;
int rightNode=2*node+2;
//构建左子树
buildTree(leftNode,start,mid);
//构建右子树
buildTree(rightNode,mid+1,end);
//子树构建号之后,更新父节点的值
tree[node]=gcd(tree[leftNode],tree[rightNode]);
}
}
//区间查询
public static int queryTree(int node,int start,int end,int L,int R){
if(L<=start&&end<=R){ //区间覆盖那就直接返回
return tree[node];
}
int mid=(start+end)/2;
int leftNode=2*node+1;
int rightNode=2*node+2;
if(R<=mid){//查询左子树
return queryTree(leftNode,start,mid,L,R);
}else if(L>mid){ //查询右子树
return queryTree(rightNode,mid+1,end,L,R);
}else{ //左右子树结果合并
int leftGcd=queryTree(leftNode,start,mid,L,R);
int rightGcd=queryTree(rightNode,mid+1,end,L,R);
return gcd(leftGcd,rightGcd);
}
}
public static int gcd(int a, int b) {
if (b == 0) {
return a;
}
int min = Math.min(a, b);
int max = Math.max(a, b);
return gcd(min, max % min);
}
public static int nextInt() throws IOException {
st.nextToken();
return (int)st.nval;
}
}
跑一个测试用例看看:
在蓝桥杯官网提交看看,整个代码是可以AC的
这里的解题思路参考了蓝桥杯官方题解:蓝桥杯官方题解