学习笔记:数字三角形模型

概念

动态规划,解决问题的一种方法。将很多问题转换成多个子问题求解,先计算子问题,到达边界直接返回问题的值,最后得到最终答案的一种方法。动态规划分为两大类:记忆化搜索和递推。记忆化搜索更好写,但常数更高;递推不太好写,但是常数低。二者时间复杂度无特殊情况基本相同。
状态转移方程:将一个问题转换成子问题计算得到结果的方程。
d p dp dp:动态规划的简称。
数字三角形,就是一个三角形,每一个点都有一个数字,找一条路径满足题目要求。形如: 1 1 1 2   3 2 3 2 3 4   5   6 4 5 6 4 5 6这样的一种图形被称为数字三角形。一般题目中要求最大值或者最小值。

方法

现在一个题目,要求下面数字三角形的路径最大值。 1 1 1 3   2 3 2 3 2 1   1   10 1 1 10 1 1 10因为我们是求最大值,则很容易想到一种方案:每次贪心选择两个点中最大的,但是在上面的三角形中会有一个问题:贪心选择的路径: 1 → 3 → 1 1\to 3\to 1 131,答案是 5 5 5。然而这个题的最优路径是 1 → 2 → 10 1\to 2\to 10 1210,答案是 13 13 13。因为我们一个较小的下面可能藏了一个大的,然而较大的下面却全是小的。所以一个完美的方式就出来了:不难发现,到达一个数的路径,就是前面两个路径的最大值加上自己。因为以自己结束的只能是从上面两个点下来才可能。但是有一个点从它结束的值是固定的:第 1 1 1排第 1 1 1个。不管怎么走,都要从它开始,这就是边界情况。设 i i i为行数, j j j为列数,从 1 1 1开始编号,于是,我们的方案就出来了: f i , j = max ⁡ ( f i − 1 , j , f i − 1 , j − 1 ) + a i , j f_{i,j}=\max(f_{i-1,j},f_{i-1,j-1})+a_{i,j} fi,j=max(fi1,j,fi1,j1)+ai,j,我们假定 f i , j = 0 , j > i ∣ ∣ j = 0 f_{i,j}=0,j>i||j=0 fi,j=0,j>ij=0。最后,我们考虑顺序。发现,到达一个数只需要知道上面两个数的 f f f即可,于是,顺序就是从第一排从上往下计算,又因为同一排的互补干扰,并不需要知道同一排的值,则在一排是什么顺序无所谓。

例题

AcWing 1015

这个题虽然变成了一个矩阵,但是仍然可以发现,每个点只能从北或西过来。只有左上角的点是一定要走的,这就是边界条件。现在,知道了解决每个点的子问题是什么,那么就很简单了。状态转移方程: f i , j = max ⁡ ( f i − 1 , j , f i , j − 1 ) + a i , j f_{i,j}=\max(f_{i-1,j},f_{i,j-1})+a_{i,j} fi,j=max(fi1,j,fi,j1)+ai,j。因为解决一个点的子问题是上面或者左边,所以从上到下,从左到右即可。

#include
using namespace std;
const int NN=104;
int f[NN][NN];
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        int n,m;
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++)
            {
                int x;
                scanf("%d",&x);
                f[i][j]=max(f[i-1][j],f[i][j-1])+x;
            }
        printf("%d\n",f[n][m]);
    }
    return 0;
}

AcWing 1018

这个题发现并不是刚才的模型。但是不难发现,想要时间最短就只能往下或往右走。于是,就变成了刚才的题目。需要注意的是,第一排和第一列需要先计算,因为这里变成了求最小,如果越界了可能不对。当然,也可以先把数组初始化为正无穷,求 min ⁡ \min min自然就不会选。

#include
using namespace std;
const int NN=104;
int f[NN][NN];
int main()
{
    int n;
    scanf("%d",&n);
    memset(f,0x3f,sizeof(f));
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
        {
            int x;
            scanf("%d",&x);
            if(i==1&&j==1)
                f[1][1]=x;
            else
                f[i][j]=min(f[i][j-1],f[i-1][j])+x;
        }
    printf("%d",f[n][n]);
    return 0;
}

AcWing 1027

法一

不难发现,这个题目和上上一道题差不多,但是要计算两次。于是可以想到一种方法:我们可以假设这个是同步进行的两个路径,毕竟两个路径怎么走除了同一点也没有什么影响。于是我们可以设 f i 1 , j 1 , i 2 , j 2 f_{i1,j1,i2,j2} fi1,j1,i2,j2为第一条路径走到 ( i 1 , j 1 ) (i1,j1) (i1,j1),第二条走到 ( i 2 , j 2 ) (i2,j2) (i2,j2)的最大值。注意,我们两个路径长度必须一样,这样才是同步进行。因为只能往下或往右走,所以长度就是 i + j i+j i+j,判断两个是否一样即可。然后,如果走到了同一点,则只加上一个点的值,反之两个路径的值都加上。然后计算状态转移,和上上题一样,只不过是两个加在一起。

#include
using namespace std;
const int NN=14;
int f[NN][NN][NN][NN],a[NN][NN];
int main()
{
    int n,x,y,w;
    scanf("%d",&n);
    while(scanf("%d%d%d",&x,&y,&w)&&(x||y||w))
        a[x][y]=w;
    for(int i1=1;i1<=n;i1++)
        for(int j1=1;j1<=n;j1++)
            for(int i2=1;i2<=n;i2++)
                for(int j2=1;j2<=n;j2++)
                    if(i1+j1==i2+j2)
                    {
                        int &d=f[i1][j1][i2][j2];
                        if(i1==i2&&j1==j2)
                            d=a[i1][j1];
                        else
                            d=a[i1][j1]+a[i2][j2];
                        d+=max(f[i1-1][j1][i2-1][j2],max(f[i1-1][j1][i2][j2-1],max(f[i1][j1-1][i2-1][j2],f[i1][j1-1][i2][j2-1])));
                    }
    printf("%d",f[n][n][n][n]);
    return 0;
}

法二

可以发现,这样会浪费计算很多东西,比如很多 i 1 + j 1 ≠ i 2 + j 2 i1+j1\not= i2+j2 i1+j1=i2+j2的情况是浪费的。于是,为了避免浪费,可以发现两个和一样,那么就统计一个和为 k k k,就有 f k , i 1 , i 2 f_{k,i1,i2} fk,i1,i2,要计算 j j j就直接用 k k k减去即可。这样,想判断是否为同一个点也十分简单,只要求 i 1 ≠ i 2 i1\not= i2 i1=i2即可。我们再来看看前面的状态转移方程分别对应什么: f i 1 − 1 , j 1 , i 2 − 1 , j 2 = f k − 1 , i 1 − 1 , i 2 − 1 f_{i1-1,j1,i2-1,j2}=f_{k-1,i1-1,i2-1} fi11,j1,i21,j2=fk1,i11,i21 f i 1 − 1 , j 1 , i 2 , j 2 − 1 = f k − 1 , i 1 − 1 , i 2 f_{i1-1,j1,i2,j2-1}=f_{k-1,i1-1,i2} fi11,j1,i2,j21=fk1,i11,i2 f i 1 , j 1 − 1 , i 2 − 1 , j 2 = f k − 1 , i 1 , i 2 − 1 f_{i1,j1-1,i2-1,j2}=f_{k-1,i1,i2-1} fi1,j11,i21,j2=fk1,i1,i21 f i 1 , j 1 − 1 , i 2 , j 2 − 1 = f k − 1 , i 1 , i 2 f_{i1,j1-1,i2,j2-1}=f_{k-1,i1,i2} fi1,j11,i2,j21=fk1,i1,i2所以,这些都可以一一对应,代码也是可行的,空间少了,浪费时间也少了。

#include
using namespace std;
const int NN=14;
int f[NN*2][NN][NN],a[NN][NN];
int main()
{
    int n,x,y,w;
    scanf("%d",&n);
    while(scanf("%d%d%d",&x,&y,&w)&&(x||y||w))
        a[x][y]=w;
    for(int k=2;k<=n*2;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                if(k-i>=1&&k-i<=n&&k-j>=1&&k-j<=n)
                {
                    f[k][i][j]=a[i][k-i];
                    if(i!=j)
                        f[k][i][j]+=a[j][k-j];
                    f[k][i][j]+=max(f[k-1][i-1][j-1],max(f[k-1][i][j-1],max(f[k-1][i-1][j],f[k-1][i][j])));
                }
    printf("%d",f[n*2][n][n]);
    return 0;
}

AcWing 275

法一

和上一题法一基本一模一样。本题不让走到同一点,那么就判断是否是同一点,是同一点就不计算。如果是从同一点过来的,那么那个点之前就没有更新,所以是零,最大值绝对不是它,所以也不会用它更新。因为两条路都会到最后一个点,所以最后一个点不会更新,直接输出差一步到达终点的两条路径即可。注意,这样的两条路径只有一个:从左边和上边来的两条路径。

#include
using namespace std;
const int NN=54;
int f[NN][NN][NN][NN],a[NN][NN];
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            scanf("%d",&a[i][j]);
    for(int i1=1;i1<=n;i1++)
        for(int j1=1;j1<=m;j1++)
            for(int i2=1;i2<=n;i2++)
                for(int j2=1;j2<=m;j2++)
                    if(i1+j1==i2+j2&&(i1!=i2||j1!=j2))
                        f[i1][j1][i2][j2]=a[i1][j1]+a[i2][j2]+max(f[i1-1][j1][i2-1][j2],max(f[i1-1][j1][i2][j2-1],max(f[i1][j1-1][i2-1][j2],f[i1][j1-1][i2][j2-1])));
    printf("%d",f[n-1][m][n][m-1]);
    return 0;
}

法二

同上一题的法二,相同点的处理同本题法一。

#include
using namespace std;
const int NN=54;
int f[NN*2][NN][NN],a[NN][NN];
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            scanf("%d",&a[i][j]);
    for(int k=3;k<=n+m;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                if(k-i>=1&&k-i<=m&&k-j>=1&&k-j<=m&&i!=j)
                    f[k][i][j]=a[i][k-i]+a[j][k-j]+max(f[k-1][i-1][j-1],max(f[k-1][i][j-1],max(f[k-1][i-1][j],f[k-1][i][j])));
    printf("%d",f[n+m-1][n-1][n]);
    return 0;
}

你可能感兴趣的:(学习笔记(提高篇),dp,动态规划,数字三角形,c++,算法)