Week16 T4 隐蔽的区间DP

题目简述

在瑞神大战宇宙射线中我们了解到了宇宙狗的厉害之处,虽然宇宙狗凶神恶煞,但是宇宙狗有一个很可爱的女朋友。
最近,他的女朋友得到了一些数,同时,她还很喜欢树,所以她打算把得到的数拼成一颗树。
这一天,她快拼完了,同时她和好友相约假期出去玩。贪吃的宇宙狗不小心把树的树枝都吃掉了。所以恐惧包围了宇宙狗,他现在要恢复整棵树,但是它只知道这棵树是一颗二叉搜索树,同时任意树边相连的两个节点的gcd(greatest common divisor)都超过1。

提示

GCD:最大公约数,两个或多个整数共有约数中最大的一个 ,例如8和6的最大公约数是2。

一个简短的用辗转相除法求gcd的例子:

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

Week16 T4 隐蔽的区间DP_第1张图片

输入样例

输入第一行一个t,表示数据组数。
对于每组数据,第一行输入一个n,表示数的个数
接下来一行有n个数​,输入保证是升序的。

输入样例:

2
2
7 17
9
4 8 10 12 15 18 33 44 81

输出样例

每组数据输出一行,如果能够造出来满足题目描述的树,输出Yes,否则输出No。
无行末空格。

输出样例:
No
Yes

思路概述

这道题第一次看到可能会认为是一道典型的T3模拟题,其题目中的相关元素特征都强烈的体现出了模拟题的特点----复杂的搜索树背景,看起来不复杂的解题逻辑,但是暴力求解了一下发现并不能很好的完成这道问题的求解。后来看了几位大佬的思路才发现是一道隐蔽的区间DP问题。

这道题是一道区间DP有两个原因:

1、每一种状态都可以表示,如果使用
d p [ i ] [ j ] dp[i][j] dp[i][j]
来表示从 i 到 j 位置能否成为一棵搜索树,那么通过枚举 i 和 j 就可以标识出所有可能出现的情况和状态。
2、状态之间可以相互转换,如果有 d p [ i ] [ j ] dp[i][j] dp[i][j] 区间内能够组合成一棵搜索树且其根的位置为k,那么如果 i-1 位置或者 j+1 位置上的数字满足
g c d ( a [ i − 1 ] , a [ k ] ) > 1 ∣ ∣ g c d ( a [ j + 1 ] , a [ k ] ) > 1 gcd(a[i-1],a[k])>1 || gcd(a[j+1],a[k])>1 gcd(a[i1],a[k])>1gcd(a[j+1],a[k])>1
那么可以推理出
[ i − 1 , j ] o r [ i , j + 1 ] [i-1,j] or[i,j+1] [i1,j]or[i,j+1]
是满足条件范围比原来大1的左子树或右子树
上面的实例就说明,可以使用已经求解出的状态来转换到未知的状态。
满足了上面的两个特性,该题目就可以使用区间DP来做。

关键的部分有三个:
1、DP的理解:
本题目需要的DP数组有三个,

DP数组 作用和表示含义
dp[i][j] 记录[i,j]区间是否能够组成搜索树
r[i]j[k] 记录以k为根,向右延伸到i,能否成为搜索树的右子树
l[j][k] 记录以k为根,向左延伸到i,能否成为搜索树的左子树

2、状态转换:
有了上面的三种数组,我们只需要考虑如何运用这些数组完成状态的转换。
当枚举到条件满足
l [ i ] [ k ] = = 1 r [ j ] [ k ] = = 1 l[i][k]==1 \\\\ r[j][k]==1 l[i][k]==1r[j][k]==1
则说明我们可以使用k作为根,[i,j]范围即组成一个搜索树。
在这个条件下还可以完成另外一组状态转换:

if(g[i-1][k]==1)
r[j][i-1]=1;
if(g[j+1][k]==1)
l[i][j+1]=1;

这里描述了两种情况:
1 、 g c d ( a [ i − 1 ] , a [ k ] ) > 1 1、gcd(a[i-1],a[k])>1 1gcd(a[i1],a[k])>1
满足该条件时,则说明已经记录下合法区间[i,j]可以作为 i -1的右子树
2 、 g c d ( a [ j + 1 ] , a [ k ] ) > 1 2、gcd(a[j+1],a[k])>1 2gcd(a[j+1],a[k])>1
满足该条件时,则说明已经记录下合法区间[i,j]可以作为 j +1的左子树

3、DP实现
了解了状态转换我们就可以开始设计DP了。
本题目中需要枚举的量有 i 到 j 这个区间,和在这个区间内的某个点 k 。本题中我们使用了
枚 举 l e n 枚 举 左 端 点 i 枚 举 区 间 中 间 点 k ∈ [ i , j ] 枚举len\\\\枚举左端点i\\\\枚举区间中间点k∈[i,j] lenik[i,j]
根据这种枚举过程,就可以实现本题目的要求

实现源码

#include
#include
#include
#include
using namespace std;
const int M=705;
bool dp[M][M];//dp[i][j]表示区间[i,j]是符合条件的区间
bool r[M][M];//r[j][k]表示k为根向右到j都是一个符合条件的右子树
bool l[M][M];//l[i][k]表示k为根向左到i都是一个符合条件的左子树
bool g[M][M];
int a[M];
int num;
int gcd(int a,int b){return b == 0 ? a : gcd(b,a%b);}

void init()
{
    memset(dp,false,sizeof(dp));
    memset(r,false,sizeof(r));
    memset(l,false,sizeof(l));
    memset(g,false,sizeof(g));
    for(int i=1;i<=num;i++)
    g[i][i]=dp[i][i]=r[i][i]=l[i][i]=true;
    for(int i=1;i<=num;i++)
    {
        for(int j=1;j<=num;j++)
        {
            if(gcd(a[i],a[j])>1)
            g[i][j]=1;
            else
            g[i][j]=0;
        }
    }
}
int main()
{
    int datagroup=0;
    scanf("%d",&datagroup);
    for(int group=0;group<datagroup;group++)
    {
        int in_num=0;
        scanf("%d",&num);a
        memset(a,0,sizeof(a));
        for(int i=1;i<=num;i++)
        scanf("%d",&a[i]);
        init();
        for(int len=1;len<=num;len++)
        {
            for(int i=1;i<=num-len+1;i++)
            {
                int j=i+len-1;
                for(int k=i;k<=j;k++)
                {
                    if(l[i][k]==1 && r[j][k]==1)
                    {
                        dp[i][j]=1;
                        if(g[i-1][k]==1)
                        r[j][i-1]=1;
                        if(g[j+1][k]==1)
                        l[i][j+1]=1;
                    }
                }
            }
        }
        if(dp[1][num]==1)
        printf("Yes\n");
        else
        printf("No\n");
    }
    return 0;

}

你可能感兴趣的:(CSP精进之路,算法,c++,动态规划)