一、题意
1.简述
2.样例
Input1
1
6
3 6 9 18 36 108
Output1
Yes
Input2
2
2
7 17
9
4 8 10 12 15 18 33 44 81
Output2
No
Yes
Hint
二、算法
主要思路
第一种思路,暴力模拟。
int insert(int root,int pelt,int st,int ed){
//root是当前子树的根节点,pelt是父节点的元素值,st和ed是该子树的元素范围
int lth = ed-st;
if(lth<=0) return 1;
int rtn;
for(int i=st;i<ed;i++){
if(root == 0||gcd(a[i],pelt)>1){
int r1 = insert(1,a[i],st,i);
int r2 = insert(1,a[i],i+1,ed);
if(r1&&r2) return 1;
}
}
return 0;
}
思路就是一个子树能不能行,就是看其左右子树是不是都行,也即存在与根节点
g
c
d
>
1
gcd>1
gcd>1的左右子树。
显然,这种递归的思路,与DP的思路的区别在于,递归会进行很多重复的操作。所以会超时。只能过前2个点(
n
≤
5
n\le5
n≤5)。
换用区间DP的思路。
一开始考虑的状态和转移方程为,设
f
[
i
]
[
j
]
[
k
]
f[i][j][k]
f[i][j][k]为以
[
a
i
,
a
j
]
[a_i,a_j]
[ai,aj]为元素范围(数据是升序的),以
a
k
a_k
ak为根节点的树是否能成立。
转移方程为:当
l
e
n
g
t
h
(
[
i
,
j
]
)
=
1
length([i,j])=1
length([i,j])=1时,
f
=
1
f=1
f=1。当
l
e
n
g
t
h
(
[
i
,
j
]
)
≤
1
length([i,j])\le1
length([i,j])≤1时,
f
[
i
]
[
j
]
[
k
]
=
f
[
i
]
[
k
−
1
]
[
k
k
1
]
∣
∣
f
[
k
+
1
]
[
j
]
[
k
k
2
]
g
c
d
(
a
[
k
k
1
]
,
a
[
k
]
)
>
1
且
g
c
d
(
a
[
k
k
2
]
,
a
[
k
]
)
>
1
f[i][j][k]=f[i][k-1][kk_1]||f[k+1][j][kk_2] \ \ \ \ \ \ gcd(a[kk_1],a[k])\gt 1 且 gcd(a[kk_2],a[k])\gt 1
f[i][j][k]=f[i][k−1][kk1]∣∣f[k+1][j][kk2] gcd(a[kk1],a[k])>1且gcd(a[kk2],a[k])>1
当然,上述式子中如果
k
−
1
k-1
k−1和
k
+
1
k+1
k+1超出了
[
i
,
j
]
[i,j]
[i,j]的范围,则直接将对应的
f
f
f项设置为
1
1
1,因为空子树是成立的。
显然,这样的DP需要
f
f
f在每个维度都需要700个空间,所以总共的空间
700
×
700
×
700
700 \times 700 \times 700
700×700×700是大于限制的。
如果将数组的空间变小一点能过前5个点。
void getF(){
for(int i=1;i<=n;i++){
f[i][i][i] = 1;
}
for(int lth=2;lth<=n;lth++){
for(int i=1;i<=n-lth+1;i++){
int ed = i+lth-1;
for(int k=i;k<=ed;k++){
bool lok = 0,rok = 0;
if(k-1>=i){
for(int kk=i;kk<=k-1;kk++){
if(f[i][k-1][kk]){
if(gcd(a[k],a[kk])>1){
lok = 1;break;
}
}
}
}
else lok = 1;
if(k+1<=ed){
for(int kk=k+1;kk<=ed;kk++){
if(f[k+1][ed][kk]){
if(gcd(a[k],a[kk])>1){
rok = 1;break;
}
}
}
}
else rok = 1;
f[i][ed][k] = lok&&rok;
}
}
}
}
继续换一种DP的思路。
状态:设
f
[
i
]
[
j
]
[
0
/
1
]
f[i][j][0/1]
f[i][j][0/1]为以
[
a
i
,
a
j
]
[a_i,a_j]
[ai,aj]为元素范围(数据是升序的),当最后一维为
0
0
0时,表示以
a
i
a_i
ai为根,以
[
a
j
,
a
i
−
1
]
[a_j,a_{i-1}]
[aj,ai−1]为左子树的子树成立(这里的成立只需要左子树成立且左子树的根节点和当前子树的根节点
g
c
d
>
1
gcd>1
gcd>1);同理,当最后一维为
1
1
1时,表示以
a
i
a_i
ai为根,以
[
a
i
+
1
,
a
j
]
[a_{i+1},a_j]
[ai+1,aj]为右子树的子树成立。
转移方程:
f
[
i
−
1
]
[
j
]
[
1
]
=
f
[
i
−
1
]
[
j
]
[
1
]
∣
∣
g
c
d
(
a
[
k
]
,
a
[
i
−
1
]
)
f
[
k
]
[
i
]
[
0
]
=
1
a
n
d
f
[
k
]
[
j
]
[
1
]
=
1
f[i-1][j][1]=f[i-1][j][1]||gcd(a[k],a[i-1]) \ \ \ \ f[k][i][0]=1\ and\ f[k][j][1]=1
f[i−1][j][1]=f[i−1][j][1]∣∣gcd(a[k],a[i−1]) f[k][i][0]=1 and f[k][j][1]=1
f
[
j
+
1
]
[
i
]
[
0
]
=
f
[
j
+
1
]
[
i
]
[
0
]
∣
∣
g
c
d
(
a
[
k
]
,
a
[
j
+
1
]
)
f
[
k
]
[
i
]
[
0
]
=
1
a
n
d
f
[
k
]
[
j
]
[
1
]
=
1
f[j+1][i][0]=f[j+1][i][0]||gcd(a[k],a[j+1]) \ \ \ \ f[k][i][0]=1\ and\ f[k][j][1]=1
f[j+1][i][0]=f[j+1][i][0]∣∣gcd(a[k],a[j+1]) f[k][i][0]=1 and f[k][j][1]=1
这样的方程只需要3层循环就够了,最后一层循环为对
k
k
k的遍历。而上次一个DP算法,则需要4层循环,所以降低了时间复杂度。但还是
O
(
n
3
)
O(n^3)
O(n3)。
三、代码
#include<iostream>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>
#include<cstdio>
#include<map>
#include<vector>
#include<queue>
using namespace std;
int gcd(int a,int b){return b == 0 ? a : gcd(b,a%b);}
int a[710];
int n;
bool f[710][710][2];//[0]表示ai为根的子树,是否左子树aj ai-1能够成立,[1]则反之
bool gcdval[710][710]; //ai与aj是否gcd>1
void getGcd(){
for(int i=1;i<=n;i++){
for(int j=i;j<=n;j++){
gcdval[i][j] = gcd(a[i],a[j])>1?1:0;
gcdval[j][i] = gcdval[i][j];
}
}
}
void getF(){
for(int i=0;i<=n;i++){
f[i][i][0] = 1; f[i][i][1] = 1;
}
for(int lth=1;lth<=n;lth++){
for(int i=1;i<=n-lth+1;i++){
int ed = i+lth-1;
for(int k=i;k<=ed;k++){
//f[i][ed][k] = 0; //更新之前先清空,可能没用
if(f[k][i][0]&&f[k][ed][1]){
f[i-1][ed][1] |= gcdval[i-1][k];
f[ed+1][i][0] |= gcdval[ed+1][k];
}
}
}
}
}
int main(){
int t;
scanf("%d",&t);
while(t--){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
memset(f,0,sizeof(f));
memset(gcdval,0,sizeof(gcdval));
getGcd();
getF();
bool isok = 0;
for(int k=1;k<=n;k++){
if(f[k][1][0]&&f[k][n][1]){
isok = 1;break;
}
}
if(isok)
printf("Yes\n");
else printf("No\n");
}
return 0;
}
/*
1
6
3 6 9 18 36 108
*/
/*
2
2
7 17
9
4 8 10 12 15 18 33 44 81
*/