首页 > 代码库 > 从一道简单的dp题中学到的...

从一道简单的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\n", 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\n", dp[n%3][k]);39   }40   return 0;41 }

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