2 5 1 2 3 4 5 5 5 4 3 2 2
看了半个上午网上的题解才基本上明白了......边界处理的好微妙...dp大法好
网上大神写的题解都好简练。。。写下自己的理解思路,顺便加强下印象
题意说的是1 - n个屌丝男,然后通过一个小黑屋也就是个栈可以改变他们的顺序,每个人有个屌丝值,然后如果这个人排第k个,则他的不高兴值为(k - 1) * D
问所有人的不高兴值之和最小是多少
先分析这个问题,对于区间1 - n,第一个人可能是第一个上场,也可能是第n个上场,其实对于第一个人,他有可能是1 - n中第k个上场(1 <= k <= n)
为什么他可以是第1 - n中任意一个上场呢,因为
比如说这样
原始队伍:1 2 3 4 5
栈底-->2 3 4 5<--栈顶 舞台 :1
1直接上舞台,剩下的四个可以直接上,也可以先进小黑屋再上,这时候1就是第一个上了
栈底-->1 2 3 4 5<--栈顶 舞台:null
栈底-->1 2 3<--栈顶 舞台:4 5
比如这两种情况,1就是最后上场的
还有在1 - n之间的
队伍:3 4 5
栈底-->1 2<--栈顶 舞台:null
然后这时先让1 2出栈上舞台
队伍:3 4 5
栈底--><--栈顶 舞台:2 1
然后3 4 5再上
栈底--><--栈顶 舞台:2 1 3 4 5
这时候1就是排第二了,其他位置也可以相似地得到
发现了子问题
因为1可以是第1 - n中任意一个上舞台,不妨设他是第k个上的,然后发现在考虑这个问题的时候考虑哪种情况都是可以分成三部分
标号为1的屌丝本身,上舞台之后排在在1之前的k - 1个,在1之后的n - k个
然后再看在1之前上舞台的k - 1个屌丝,他们可以以同样的分析方法进行更小范围的独立分析,在1之后的n - k个也可以
1在第k个上舞台时,他的不高兴值为a[i] * (k - 1),那么只要知道他之前的所有屌丝的不高兴值之和的最小值sumpre,和他之后的所有屌丝的不高兴值之和的最小值之和sumnext,最后1 - n上的最小值就是
a[i] * (k - 1) + sumpre + sumnext
sumpre和sumnext又可以以同样的方式递归分析下去
直到精确到不可再分的区间长度为一的个人
定义函数int solve(i, j)函数返回区间[i, j]上的题目要求的最小值,那么
solve(i, j) = min(solve(i + 1, i + k - 1) + solve(i + k, j) + a[i] * (k - 1) + (sum[j] - sum[i + k - 1]) * k) (1 <= k <= j - i + 1)
区间[i, j]
i i + 1, i + 2, i + 3, ..........,i + k - 1 i + k ..........i + k + 1.........j
第k个上 之前的k - 1个 之后的 j - i - k + 1个
因为在第i之后的那个区间[i + k, j]在分析的时候是内部独立考虑的,因为在考虑这个子问题的时候肯定不知道它外面的父问题是什么
所以在[i + k, j]上的最优解只是独立考虑[i + k, j]的最小值,在综合到[i, j]这个区间时由于[i, i + k - 1]这k个都在他们前面所以
[i + k, j]这个最优解还要再加上D(i + k) * k + D(i + k + 1) * k + ...... + D(j) * k, D(x)为第x个屌丝的屌丝值
如果用前缀和来表示的话就是前j个的屌丝值和sum[j],前i + k - 1个的屌丝值和sum[i + k - 1]
D(i + k) * k + D(i + k + 1) * k + ...... + D(j) * k = (sum[j] - sum[i + k - 1]) * k
边界
发现在k = 1时
solve(i + 1, i) + solve(i + 1, j) + a[i] * (1 - 1) + (sum[j] - sum[i]) * 1
这个时候也就是第i个是排在[i, j]这个区间的第一个,可见他之前没有别人,从i + 1到j全在他后面
而且有个没有意义的solve(i + 1, i)明显要让这个值有意义只有让它等于0
solve(i + 1, j) + (sum[j] - sum[i])
在k = j - i + 1时
solve(i + 1, j) + solve(j + 1, j) + a[i] * (j - i) + (sum[j] - sum[j]) * (j - i + 1);
这个时候i排在[i, j]这个区间最后一个,从i + 1到j全在他前面
这时候又出现了solve(j + 1, j)没有意义,也是明显他要等于0,而且因为都在i前面,所以[i + 1, j]上的所有屌丝都不用补(sum[j] - sum[i + k - 1]) * k) 了,当然为0
solve(i + 1, j) + a[i] * (j - i)
实现
记忆化搜索,由于系统调用栈,相对慢一些
//记忆化搜索 #include <iostream> #include <cstring> #include <algorithm> using namespace std; const int maxn = 100 + 10, INF = 0x3f3f3f3f; int n, a[maxn], sum[maxn], dp[maxn][maxn]; int solve(int i, int j) { if (i > j) return 0; int ans = dp[i][j]; if (ans != -1) return ans; ans = INF; for (int k = 1; k <= j - i + 1; k++) { ans = min(ans, solve(i + 1, i + k - 1) + solve(i + k, j) + a[i] * (k - 1) + (sum[j] - sum[i + k - 1]) * k); } return dp[i][j] = ans; } int main() { int T; scanf("%d", &T); for (int t = 1; t <= T; t++) { scanf("%d", &n); sum[0] = 0; for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); sum[i] = sum[i - 1] + a[i]; } memset(dp, -1, sizeof(dp)); printf("Case #%d: %d\n", t, solve(1, n)); } return 0; }
开数组直接递推,很快
形象来看像是这么个过程,就是父问题去覆盖子问题的过程,每次的覆盖范围都在扩大,最终扩大到n,将所有的子问题的
解都覆盖了,也就是所有子问题向上构成了最终问题的解,dp的奇妙之处啊啊啊啊啊啊啊啊啊。。。。
//递推 #include <iostream> #include <cstring> #include <algorithm> using namespace std; const int maxn = 100 + 10, INF = 0x3f3f3f3f; int n, a[maxn], sum[maxn], dp[maxn][maxn]; int main() { int T; scanf("%d", &T); for (int t = 1; t <= T; t++) { scanf("%d", &n); sum[0] = 0; for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); sum[i] = sum[i - 1] + a[i]; } for (int i = 0; i <= n; i++) { for (int j = 0; j <= n; j++) { if (i > j) dp[i][j] = 0; else dp[i][j] = INF; } } /*l为计算的区间长度 要先有子问题的最优解才能向上推出更大区间的最优解 所以先是区间长度为1的最优解,然后为2。。。一直到整个区间[1, n] */ for (int l = 1; l <= n; l++) { for (int i = 1; i <= n; i++) { int j = i + l - 1; if (j > n) continue; //防越界 for (int k = 1; k <= j - i + 1; k++) { dp[i][j] = min(dp[i][j], dp[i + 1][i + k - 1] + dp[i + k][j] + (k - 1) * a[i] + (sum[j] - sum[i + k - 1]) * k); } } } printf("Case #%d: %d\n", t, dp[1][n]); } return 0; }