从一道简单的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吧!