zoukankan      html  css  js  c++  java
  • 从一道简单的dp题中学到的...

    今天想学点动态规划的知识,于是就看了杭电的课件,数塔问题啊,LCS啊都是比较经典的动规了,然后随便看了看就开始做课后练习题。。。

    HDOJ 1421 搬寝室

    http://acm.hdu.edu.cn/showproblem.php?pid=1421

    题目大意:从n(n <= 2000)个数中选出k对数(即2*k个),使它们的差的平方和最小。

    例如:从8,1,10,9,9中选出2对数,要使差的平方和最小,则应该选8和9、9和10,这样最小,结果为2


    因为知道是dp的题,先建个dp[][]数组,然后我就一直在想从 n 个数中选出 k 对,和从 n-1 个数中选出 k 对有什么关系...

    妹夫的,想了半天没想出来有关系,一搜题解,看呆了,排序两个大字赫然映入我的眼帘...哎呀,笨死了,我这么多年饭算是白吃了...

    只要先把这n个数排好序,那么新加入的数如果要使用,只能是跟倒数第二个数(第n-1个)作差(未排序之前不能保证这一点,所以无法选择),那么从n个数中选出k对数,就有两个状态来源:

    1、如果没用上第n个数,那么dp[n][k] = dp[n-1][k]; 

    2、如果用上了第n个数,那么dp[n][k] = dp[n-2][k-1] + (a[n] - a[n-1])^2

    只要取二者的min()就ok了,得到状态转移方程是:dp[i][j] = min(dp[i-1][j], dp[i-2][j-1] + (a[i] - a[i-1]) * (a[i] - a[i-1]));

    编程如下:

     1 #include<cstdio>
     2 #include<cstring>
     3 #include<cmath>
     4 #include<algorithm>
     5 #define INF 0x3f3f3f3f
     6 #define MAXN 2005
     7 using namespace std;
     8 int a[MAXN];
     9 int dp[MAXN][MAXN];
    10 int main()
    11 {
    12   int n, k;
    13   while(scanf("%d%d", &n, &k) != EOF)
    14   {
    15     for (int i = 1; i <= n; ++i)
    16     {
    17       scanf("%d", &a[i]);
    18     }
    19     memset(dp, 0x3f, sizeof(dp));
    20     for (int i = 0; i < n + 1; ++i)
    21     {
    22       dp[i][0] = 0;
    23     }
    24 
    25     sort(a + 1, a + n + 1);
    26     for (int i = 2; i <= n; ++i)
    27     {
    28       for (int j = 1; j <= k, j <= i / 2; ++j)
    29       {
    30         dp[i][j] = min(dp[i-1][j], dp[i-2][j-1] + (a[i] - a[i-1]) * (a[i] - a[i-1]));
    31       }
    32     }
    33     printf("%d
    ", dp[n][k]);
    34   }
    35   return 0;
    36 }

    插个小插曲: 关于int最大值的定义,此处是写了 #define INF 0x3f3f3f3f....

    我们知道32-int的最大值其实是0x7fffffff; 在这个程序中定义成这个值也没有问题,下面谈一下它们两个的用处区别:

    1、如果定义的最大值只是单纯的用于比较,比如初始化一个min时,这样的情景把INF设为0x7fffffff;

    2、很多时候我们定义一个最大值,并不是单纯的比较,而且还有松散的操作,比如这样:

    在很多最短路径算法中都有这段代码

    if (d[u]+w[u][v]<d[v]) d[v]=d[u]+w[u][v];
    我们知道如果u,v之间没有边,那么w[u][v]=INF,如果我们的INF取0x7fffffff,那么d[u]+w[u][v]会溢出而变成负数。

    也就是说对于一个无穷大,应该加上一个常数还是无穷大,而0x7fffffff + 1 就溢出了,显然不符合这一基本要求。

    于是,我们发现了0x3f3f3f3f(不知道哪位大神最先使用这个数的),它的值是1061109567,也就是10^9级别的,和0x7fffffff一个数量级,它加上一个常数仍然还在32int的范围内...更加地,一个无穷大还应该满足,正无穷大 + 正无穷大 还是无穷大,恰好地,0x3f3f3f3f + 0x3f3f3f3f = 2122219134,这个数非常大但却没有超过32-bit int的表示范围,所以0x3f3f3f3f还满足了我们“无穷大加无穷大还是无穷大”的需求。

    最精妙的是,我们知道在使用memset()函数给数组赋初值的时候,一般使用0,-1,true,false这四个参数,其他的可能会出现达不到想要结果的情形(比如全赋1,可能就不是1),因为memset是按字节操作的,它能够对数组清零是因为0的每个字节都是0,现在好了,如果我们将无穷大设为0x3f3f3f3f,那么奇迹就发生了,0x3f3f3f3f的每个字节都是0x3f!所以要把一段内存全部置为无穷大,我们只需要memset(a,0x3f,sizeof(a))。

    综上,0x3f3f3f3f真的是在赋最大值时一个很不错的选择。


    好的,说回到这个程序上,按照上面写的代码,提交之后,虽然能够AC,但是运行时间是800+ms,空间是15000+K,这显然不是我们想看到的结果...

    那么就开始优化了,看了一下程序,显然耗时的过程不是对dp[][]求值的过程,因为输入的多组数据中,不是每组都需要求到n=2000,k=1000的,显然是在使用memset()给dp[][]赋初值的时候,由于每组数据都要初始化,所以每次都是一个MAXN^2的时间,将其改为根据n,k初始化即可,没有什么技术含量...

    对于空间的优化,空间占用显然是因为我无脑的开了一个int dp[MAXN][MAXN];

    我们通过研究程序发现在求解dp[i][j]的过程中,对 i 的值,实际上我们只需要保存三组就够了,因为dp[i][j]最多只会用到dp[i-1][]和dp[i-2][],对于dp[i-3][]和之前的数据是没有引用的,也就是说这些数据都是无效的了...我们只需要开一个int dp[3][MAXN/2];  (k不超过n的一半...)

    这样的数组叫做“滚动数组”,相当于在原数组中每次在求解完dp[i][]之后,就把它写到dp[i-3][]那里,只用3个位置,通过这样的滚动,就完成了MAXN长度的求值,这种技巧在求解dp问题中非常常见。它对于时间上没有任何帮助,但是在空间上节省的就不是一点半点了(想想2005 * 2005 和 3 * 2005)...

    采用“滚动数组”的技巧,我们顺带的把dp[][]的初始化用时也降低了...只需把原程序中的状态转移方程改为如下即可:

    dp[i%3][j] = min(dp[(i-1)%3][j], dp[(i-2)%3][j-1] + (a[i] - a[i-1]) * (a[i] - a[i-1]));

    改完之后的代码如下,红色部分就是改动内容:

     1 #include<cstdio>
     2 #include<cstring>
     3 #include<cmath>
     4 #include<cstdlib>
     5 #include<algorithm>
     6 #define INF 0x3f3f3f3f
     7 #define MAXN 2005
     8 using namespace std;
     9 int a[MAXN];
    10 int dp[3][MAXN/2];
    11 int main()
    12 {
    13   int n, k;
    14   while(scanf("%d%d", &n, &k) != EOF)
    15   {
    16       
    17     for (int i = 1; i <= n; ++i)
    18     {
    19       scanf("%d", &a[i]);
    20     }
    21     for (int i = 0; i <= 2; ++i)
    22     {
    23       for (int j = 1; j < k + 1; ++j)
    24       {
    25         dp[i][j] = INF;
    26       }
    27       dp[i][0] = 0;
    28     }
    29 
    30     sort(a + 1, a + n + 1);
    31     for (int i = 2; i <= n; ++i)
    32     {
    33       for (int j = 1; j <= k; ++j)
    34       {
    35         dp[i%3][j] = min(dp[(i-1)%3][j], dp[(i-2)%3][j-1] + (a[i] - a[i-1]) * (a[i] - a[i-1]));
    36       }
    37     }
    38     printf("%d
    ", dp[n%3][k]);
    39   }
    40   return 0;
    41 }

    这样改完之后,再次提交,时间是 90ms,空间是 240K,已经进入了此题solution排行榜的第一页,算是比较理想的结果了(对比之前 800ms, 15000K),有兴趣的读者再去对输入输出等等细节方面优化一下,冲击一下best solution吧!

  • 相关阅读:
    微信小程序-物流api
    flutter第一课
    git新手配置(ios环境)
    git新手配置(windows环境)
    .Net Core WebApi(三)——操作Oracle数据库
    .Net Core WebApi(二)——添加Nlog
    .Net Core WebApi(一)——添加Swagger
    SQL server、Oracle中拿到新增列时的自增字段值
    Sql Server中如何删除字段的自增标识
    Sql Server中删除一个字段的默认值
  • 原文地址:https://www.cnblogs.com/grubbyskyer/p/3854143.html
Copyright © 2011-2022 走看看