石子合并 区间dp
题目链接:http://acm.nyist.edu.cn/JudgeOnline/problem.php?pid=737
题目大意: 有N堆石子排成一排,每堆石子有一定的数量。现要将N堆石子并成为一堆。合并的过程只能每次将相邻的两堆石子堆成一堆,每次合并花费的代价为这两堆石子的和,经过N-1次合并后成为一堆。求出总的代价最小值。
题目分析:区间dp
参考博客
区间dp最简单形式的伪代码
//mst(dp,0) 初始化DP数组
for(int i=1;i<=n;i++)
{
dp[i][i]=初始值
}
for(int len=2;len<=n;len++) //区间长度
for(int i=1;i<=n;i++) //枚举起点
{
int j=i+len-1; //区间终点
if(j>n) break; //越界结束
for(int k=i;k<j;k++) //枚举分割点,构造状态转移方程
{
dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]+w[i][j]);
}
}
石子合并代码:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 500 + 100;
const int INF = 0x3f3f3f3f;
int dp[maxn][maxn], data[maxn], sum[maxn];
int main()
{
int n;
while(~scanf("%d", &n)) {
memset(dp, INF, sizeof(dp));
for(int i = 1; i <= n; i++) {
scanf("%d", &data[i]);
sum[i] = sum[i - 1] + data[i];
}
for(int i = 1; i <= n; i++) dp[i][i] = 0;
for(int len = 2; len <= n; len++) {
for(int i = 1; i < n; i++) {
int j = i + len - 1;
if(j > n) break;
for(int k = i; k < j; k++) {
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]);
}
}
}
printf("%d\n", dp[1][n]);
}
}
平行四边形优化
刚刚的代码时间复杂度是n^3,可以优化为n^2。
具体做法:
由于状态转移时是三重循环的,我们想能否把其中一层优化呢?尤其是枚举分割点的那个,显然我们用了大量的时间去寻找这个最优分割点,所以我们考虑把这个点找到后保存下来。
用s[i][j]表示区间[i,j]中的最优分割点,那么第三重循环可以从[i,j-1)优化到【s[i][j-1],s[i+1][j]】。(这个时候小区间s[i][j-1]和s[i+1][j]的值已经求出来了,然后通过这个循环又可以得到s[i][j]的值)。
优化后的代码如下:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 500 + 100;
const int INF = 0x3f3f3f3f;
int dp[maxn][maxn], data[maxn], sum[maxn], s[maxn][maxn];
int main()
{
int n;
while(~scanf("%d", &n)) {
memset(dp, INF, sizeof(dp));
for(int i = 1; i <= n; i++) {
scanf("%d", &data[i]);
sum[i] = sum[i - 1] + data[i];
s[i][i] = i;
}
for(int i = 1; i <= n; i++) dp[i][i] = 0;
for(int len = 2; len <= n; len++) {
for(int i = 1; i < n; i++) {
int j = i + len - 1;
if(j > n) break;
for(int k = s[i][j - 1]; k <= s[i + 1][j]; k++) {
if(dp[i][j] > dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]) {
dp[i][j] = dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1];
s[i][j] = k;
}
}
}
}
printf("%d\n", dp[1][n]);
}
}
对于石子合并问题,有一个有一个最好的算法,那就是GarsiaWachs算法。时间复杂度为O(n^2)。
它的步骤如下:
设序列是stone[],从左往右,找一个满足stone[k-1] <= stone[k+1]的k,找到后合并stone[k]和stone[k-1],再从当前位置开始向左找最大的j,使其满足stone[j] > stone[k]+stone[k-1],插到j的后面就行。一直重复,直到只剩下一堆石子就可以了。在这个过程中,可以假设stone[-1]和stone[n]是正无穷的。
举个例子:
186 64 35 32 103
因为35<103,所以最小的k是3,我们先把35和32删除,得到他们的和67,并向前寻找一个第一个超过67的数,把67插入到他后面,得到:186 67 64 103,现在由5个数变为4个数了,继续:186 131 103,现在k=2(别忘了,设A[-1]和A[n]等于正无穷大)234 186,最后得到420。最后的答案呢?就是各次合并的重量之和,即420+234+131+67=852。
基本思想是通过树的最优性得到一个节点间深度的约束,之后证明操作一次之后的解可以和原来的解一一对应,并保证节点移动之后他所在的深度不会改变。具体实现这个算法需要一点技巧,精髓在于不停快速寻找最小的k,即维护一个“2-递减序列”朴素的实现的时间复杂度是O(n*n),但可以用一个平衡树来优化,使得最终复杂度为O(nlogn)。
转自上面的那个博客链接。
代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 4e4+100;
const int INF = 0x7fffffff;
int stone[N];
int n,t,ans;
void combine(int k)
{
int tmp = stone[k] + stone[k-1];
ans += tmp;
for(int i=k;i<t-1;i++)
stone[i] = stone[i+1];
t--;
int j = 0;
for(j=k-1;stone[j-1] < tmp;j--)
stone[j] = stone[j-1];
stone[j] = tmp;
while(j >= 2 && stone[j] >= stone[j-2])
{
int d = t - j;
combine(j-1);
j = t - d;
}
}
int main()
{
while(~scanf("%d",&n))
{
for(int i=1;i<=n;i++)
scanf("%d",stone+i);
stone[0]=INF;
stone[n+1]=INF-1;
t = 3;
ans = 0;
for(int i=3;i<=n+1;i++)
{
stone[t++] = stone[i];
while(stone[t-3] <= stone[t-1])
combine(t-2);
}
while(t > 3) combine(t-1);
printf("%d\n",ans);
}
return 0;
}
石子合并 环形版
题目大意:环形石子合并,即现在有围成一圈的若干堆石子,其他条件跟其那面那题相同,问合并所需最小代价。
题目分析:我们需要做的是尽量向简单的问题转化,可以把前n-1堆石子一个个移到第n个后面,那样环就变成了线,即现在有2*n-1堆石子需要合并。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 500 + 100;
const int INF = 0x3f3f3f3f;
int dp[maxn][maxn], data[maxn], sum[maxn], DP[maxn][maxn];
int main()
{
int n;
while(~scanf("%d", &n))
{
memset(dp, 0, sizeof(dp));
memset(DP, INF, sizeof(DP));
for(int i = 1; i <= n; i++)
{
scanf("%d", &data[i]);
data[i + n] = data[i];
sum[i] = sum[i - 1] + data[i];
}
for(int i = 1; i <= 2 * n; i++)
{
sum[i] = sum[i - 1] + data[i];
}
// for(int i = 1; i <= 2 * n; i++) printf("%d%c", sum[i], i == 2 * n ? '\n' : ' ');
for(int i = 1; i <= 2 * n; i++) DP[i][i] = 0;
for(int len = 2; len <= n; len++)
{
for(int i = 1; i <= 2 * n; i++)
{
int j = i + len - 1;
if(j >= 2 * n) break;
for(int k = i; k < j; k++)
{
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]);
DP[i][j] = min(DP[i][j], DP[i][k] + DP[k + 1][j] + sum[j] - sum[i - 1]);
}
}
}
int ans1 = INF, ans2 = -INF;
for(int i = 1; i <= n; i++) {
ans1 = min(ans1, DP[i][i + n - 1]);
ans2 = max(ans2, dp[i][i + n - 1]);
}
//ans1最小值 ans2最大值
printf("%d %d\n", ans1, ans2);
}
}