您的位置:首页 > 大数据 > 人工智能

zoj 1163 The Staircases 动态规划(dp)

2011-03-12 23:42 477 查看
这道题目通过dp来解决,而且有许多解决方法,它们在时间和空间上的复杂度都不相同。让我们直接开始看这些解决方案:

方案一:

这是我最初研究出来的方案,是我本人当前智力能力所能想出来的最佳方案。虽然这个方案很笨,但是我相信会有人和我一样这么想的。

当然我还是使用dp的,只是转化方程比较土鳖一点而已。考虑两个参数积木数N,台阶数S(有几级台阶),那么可以知道:f(N,S) = f(N-S, S-1) + f(N-2S, S-1) + f(N-3S, S-1) + .... + f(N-kS, S-1)。原理就是说考虑第一级的高度,如果为一,那么我们去掉每一级的最底下的那块积木,那么就剩下S-1级了,因为第一级没有了。同理如果高度为2,为3,。。。

这个方法可以搞定,而且我的代码也成功AC了。然后洋洋得意了一阵子,发现居然还有其他方法,而且相比之下,我这个方法就像是从乡下来的土鳖,真是令人丧气。

方案二:

网上可以搜到的主流解法,也是大部分人可以到达的境界(为什么我到不了呢?郁闷)。考虑最高级的台阶,也就是最右边一列的台阶,假设它的高度不大于h(注意,不是等于h)。那么我们可以得到状态转移方程:f(N, h) = f(N-h, h-1) + f(N, h-1),什么意思呢?就是说,计算N,h的过程可以拆分为两种情况:

最高级的台阶正好高度为h,那么也就是剩下的N-h块积木在h-1的情况

最高级的台阶高度小于h,这个你懂的

而题目要求得到N的所有摆放方式,那么就是f(N,N)-1,注意这里的-1表示单独的一列,也就是只有一级台阶,这个是题目要求摒弃的。当然dp还有一个事情要确定就是初始条件,根据题意以及前面所说的定义方式,f(0,i)=1。然后就可以得到类似下面的代码了。

#include<stdio.h>
#include<iostream>
using namespace std;
int main()
{
#ifndef ONLINE_JUDGE
    freopen("input.txt", "rt", stdin);
    freopen("output.txt", "wt", stdout);
#endif
    double m[501][501];
    for(int i=0;i<501;i++)
    {
        /*没研究到底可不可以去掉,反正去掉的可以AC
        for(int j=0;j<501;j++)
        {
            m[i][j] = 0;
        }*/
         m[0][i] = 1;
   }
    for(int i=1;i<=500;i++)
    {
        for(int j=1;j<=500;j++)
        {
            if(i>=j)
            {
                m[i][j] = m[i-j][j-1]+m[i][j-1];
            }else
            {
                m[i][j] = m[i][j-1];
            }
        }
    }
    int N;
    cin >> N;
    while(N)
    {
        printf("%0.0lf/n",m

-1);
        cin >>N;
    }
}


注意这个题目的另外一个陷阱,就是用long会溢出,你可以自己把N接近500的答案打出开看看,发现32位是不够的。好了,大家看到这里基本可以回去开心的code,然后帅气的AC了。

什么?你还不满足?

那好,我还有更好的解决方案。

方案三:

是的,这是一个更加好的解决方案。但是,它做的只是空间的优化,同时对AC基本没有影响。如果你要赶紧去陪mm压马路的话那就Alt+F4吧。

好的,下面隆重介绍经过我研究了n天之后才完全搞明白的方案(不要bs我笨)。这个方案是对方案二的空间优化,优化方法一般也称为滚动数组,将方案二中的数组m[501][501]优化为m[501],也就是从O(N2)优化为O(N)。

考虑状态转移公式:f(N, h) = f(N-h, h-1) + f(N, h-1)。 我们可以发现相对应每个h的计算,都只是用到了h-1的数值,而与其他高度h的数值完全没有关系。参照方案二的代码,也就是说每一列的数值只是与前一列的的数值相关(j和j-1列)。

为了看的更加清楚,我们将二维数组m的两个维度交换一下(也就是i和j交换),状态方程就变为f(h, N) = f(h-1, N-h) + f(h-1, N),变成如下的形式:

double m[501][501];
    for(int i=0;i<501;i++)
    {
         m[i][0] = 1;
   }

    for(int i=1;i<=500;i++)
    {
        for(int j=1;j<=500;j++)
        {
            if(j>=i)
            {
                m[i][j] = m[i-1][j-i] + m[i-1][j];//两个维度交换了
            }else
            {
                m[i][j] = m[i-1][j];
            }
        }
    }


现在要压缩m数组了,考虑在计算i行时,我们只需要i-1行的数值,而在i行计算结束之后i-1行的数据只需要留下m[i-1][i-1]就可以了(这是N=i-1的解)。现在我们使用一维数组m[501],同时假设,i-1行的数据已经搞定,整行存在m[501]这个一维数组中,那么计算i行的时候如果将内循环改为从大到小循环,情况会如何呢?这个时候可以发现,每次计算相应下标为j的值时,所使用的下标小于j的值,正是之前存在数组中的i-1行的数据,也就是说它依然是正确的。

现在需要解决另外一个问题,数据覆盖的问题,我们可以看到如果j从500循环到1,那么整个数组都被覆盖了,显然这是不行的,因为小于i的解没有存下来。所以这里的循环应该是从500到i,而下标小于i(比如为k)的数值正好对应了N=k的解。

证明:初始状态,m[1]就是N=1的解。递归转移:计算第i行的时候,观察在j<=i的时候,也就是内循环中的else分句,m[i][j] = m[i-1][j],如果i-1<=j,依然有m[i-1][j] = m[i-2][j],所以最终可以得到m[i][j] = m[j][j],也就是N=j的解,而这个解正好存在下标为j的位置上。因此这个计算依然是正确的。

如果难以理解上面两段,可以去http://www.oiers.cn/pack/P01.html看看其中的空间复杂度优化,我就是在这里面学习的。同时参照下面的代码,希望诸位可以耐心看懂,牢记在心。

#include<stdio.h>
#include<iostream>
using namespace std;
int main()
{
#ifndef ONLINE_JUDGE
    freopen("input.txt", "rt", stdin);
    freopen("output.txt", "wt", stdout);
#endif

    double m[501];
    m[0] = 1;
    for(int i=1;i<501;i++)
        m[i] = 0;
    for(int i=1;i<501;i++)
    {
        for(int j=500;j>=i;j--)
        {
            m[j] = m[j-i] + m[j];
        }
    }
    int N;
    cin >> N;
    while(N)
    {
        printf("%0.0lf/n",m
-1);
        cin >>N;
    }
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: