NOI 1997 积木游戏 (COGS 261) DP

是的没错就是这道题,在我上一篇博客中所说的DP的题。
为什么它卡了我这么长时间呢,因为我的做法比较奇葩(归根结底还是DP比较弱)。

我是这么想的:f[i][j]表示前i个积木,堆成j堆的最大高度和。这样想的状态的话方程就很好写了:f[i][j] = max(f[i][j], f[k][j-1]+h[k+1][i]) (j<=k<=i-1) 既好想又快,何乐而不为,那么我就确定了这个思路。
之后就要求h数组,那么先想清楚h[i][j]表示的是[i,j]这个区间内的木块能叠出的最大高度,并不是以i为最底下的那一块。怎么求呢,一开始写的搜索,想一共n才100,应该不会T吧,也没有仔细算。
void sec(int beg, int la, int lb, int high, int i){
	if(i > n) return;
	h[beg][i] = max(h[beg][i-1], h[beg][i]);
	sec(beg, la, lb, high, i+1);
	if(a[i]<=la && b[i]<=lb){
		h[beg][i] = max(h[beg][i], high+c[i]);
		sec(beg, a[i], b[i], high+c[i], i+1);
	}
	if(b[i]<=la && c[i]<=lb){
		h[beg][i] = max(h[beg][i], high+a[i]);
		sec(beg, b[i], c[i], high+a[i], i+1);
	}
	if(a[i]<=la && c[i]<=lb){
		h[beg][i] = max(h[beg][i], high+b[i]);
		sec(beg, a[i], c[i], high+b[i], i+1);
	}
}
但是它华丽丽的T了4个点,在n>60的时候就过不去了。

这时我也看了一下题解,大家用的比较多的状态就是那个四维的,后面我再论述。看了题解后觉得那个写起转移来太麻烦,对于边界以及初始化很容易出错,所以决定坚定不移地想方法去求h[i][j]。

因为h[i][j]表示的是区间内的积木叠出的最大高度,以哪个开始并不确定,所以稍加思考就知道它并不满足最优子结构,不能再通过DP来求。之后咨询了前辈,单问怎么求这个h数组:建图。
于是可以想到在图上跑最长路:以高为边,当i可以放到j上面时建一条j到i的边,边权为i的高。再仔细想,因为每一个积木是有三种状态的:分别以三条边为高。这三种状态对应的底面边长也就不同,这样建边的条件也就不同,那么也就是说,每个点需要被拆成三个点,再从前往后建图。具体建图的做法是:
memset(dis, -0x3f, sizeof dis);
	for(int i = 1; i < n; i++)
	for(int j = i+1; j <= n; j++){
		if(a[i]>=a[j]&&b[i]>=b[j]) dis[i*3+0][j*3+0] = c[j];
		if(a[i]>=a[j]&&b[i]>=c[j]) dis[i*3+0][j*3+1] = b[j];
		if(a[i]>=b[j]&&b[i]>=c[j]) dis[i*3+0][j*3+2] = a[j];
		if(a[i]>=a[j]&&c[i]>=b[j]) dis[i*3+1][j*3+0] = c[j];
		if(a[i]>=a[j]&&c[i]>=c[j]) dis[i*3+1][j*3+1] = b[j];
		if(a[i]>=b[j]&&c[i]>=c[j]) dis[i*3+1][j*3+2] = a[j];
		if(b[i]>=a[j]&&c[i]>=b[j]) dis[i*3+2][j*3+0] = c[j];
		if(b[i]>=a[j]&&c[i]>=c[j]) dis[i*3+2][j*3+1] = b[j];
		if(b[i]>=b[j]&&c[i]>=c[j]) dis[i*3+2][j*3+2] = a[j];
	}
看起来虽然很多很乱,其实复制粘贴就好,首先i向j连边的第一个要求就是i<j,因为叠积木要求下面的编号小于上面的。之后就是把每个点拆成三个点:i*3,i*3+1,i*3+2,三个点对应不同的连边的条件:就是底面的两个边长。(为了方便,在读入的时候进行排序,使a[i]<=b[i]<=c[i])一共300个点,用邻接矩阵存图,很显然要用floyd跑最长路。
最后dis[i][j]代表的是什么含义呢?首先注意我们建边时,i到j连边时边权是j的高度,没有算最底下的i的高度,到后面用dis计算h的时候是要加上的。dis[i][j]代表的是以i为最下面的积木,j为最上面的积木时的高度。所以h[i][j]是max{dis[i'][j']} (i<=i'<=j'<=j)
	for(int i = 1; i < n*3+2; i++)
	for(int j = i+1; j <= n*3+2; j++){
		if(i%3 == 0)	 h[i/3][j/3] = max(max(dis[i][j],0)+c[i/3], h[i/3][j/3]);	
		else if(i%3 == 1)h[i/3][j/3] = max(max(dis[i][j],0)+b[i/3], h[i/3][j/3]);	
		else if(i%3 == 2)h[i/3][j/3] = max(max(dis[i][j],0)+a[i/3], h[i/3][j/3]);	
	}
	
	for(int i = 1; i < n; i++)
	for(int j = i+1; j <= n; j++)
	for(int k = i+1; k < j; k++){
		h[i][j] = max(h[i][j], h[i][k]);
		h[i][j] = max(h[i][j], h[k][j]);
	}
第一次两个循环是先把拆开后的节点转换为原来的点(别忘记还要加上刚刚所说的最底下的i的高度),第二次的三层循环就是求出真正的h数组了。

这下终于在O(N^3)内求出h数组,就可以快乐的使用自己奇葩的做法AC了。
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

int n, m, a[101], b[101], c[101], f[101][101], h[101][101];
int dis[303][303]; 

int d(int i, int j){
	if(f[i][j]) return f[i][j];
	for(int k = j-1; k < i; k++)
		f[i][j] = max(f[i][j], d(k, j-1)+h[k+1][i]);
	return f[i][j];
}

int main()
{
	scanf("%d %d", &n, &m);
	for(int i = 1; i <= n; i++){
		scanf("%d %d %d", a+i, b+i, c+i);
		if(a[i]>b[i]) swap(a[i], b[i]);
		if(a[i]>c[i]) swap(a[i], c[i]);
		if(b[i]>c[i]) swap(b[i], c[i]);
	}
	
	memset(dis, -0x3f, sizeof dis);
	for(int i = 1; i < n; i++)
	for(int j = i+1; j <= n; j++){
		if(a[i]>=a[j]&&b[i]>=b[j]) dis[i*3+0][j*3+0] = c[j];
		if(a[i]>=a[j]&&b[i]>=c[j]) dis[i*3+0][j*3+1] = b[j];
		if(a[i]>=b[j]&&b[i]>=c[j]) dis[i*3+0][j*3+2] = a[j];
		if(a[i]>=a[j]&&c[i]>=b[j]) dis[i*3+1][j*3+0] = c[j];
		if(a[i]>=a[j]&&c[i]>=c[j]) dis[i*3+1][j*3+1] = b[j];
		if(a[i]>=b[j]&&c[i]>=c[j]) dis[i*3+1][j*3+2] = a[j];
		if(b[i]>=a[j]&&c[i]>=b[j]) dis[i*3+2][j*3+0] = c[j];
		if(b[i]>=a[j]&&c[i]>=c[j]) dis[i*3+2][j*3+1] = b[j];
		if(b[i]>=b[j]&&c[i]>=c[j]) dis[i*3+2][j*3+2] = a[j];
	}
	
	for(int k = 1; k <= n*3+2; k++)
	for(int i = 1; i <= n*3+2; i++)
	for(int j = 1; j <= n*3+2; j++)
		if(i<k && k<j)
		dis[i][j] = max(dis[i][k]+dis[k][j], dis[i][j]);
	
	for(int i = 1; i < n*3+2; i++)
	for(int j = i+1; j <= n*3+2; j++){
		if(i%3 == 0)	 h[i/3][j/3] = max(max(dis[i][j],0)+c[i/3], h[i/3][j/3]);	
		else if(i%3 == 1)h[i/3][j/3] = max(max(dis[i][j],0)+b[i/3], h[i/3][j/3]);	
		else if(i%3 == 2)h[i/3][j/3] = max(max(dis[i][j],0)+a[i/3], h[i/3][j/3]);	
	}
	
	for(int i = 1; i < n; i++)
	for(int j = i+1; j <= n; j++)
	for(int k = i+1; k < j; k++){
		h[i][j] = max(h[i][j], h[i][k]);
		h[i][j] = max(h[i][j], h[k][j]);
	}
	for(int i = 1; i <= n; i++)
		f[i][1] = h[1][i];

	printf("%d", d(n, m));
}

最后还是提一下大家都用的思路把:f[i][j][k][l]表示前i个,分成j堆,上一个叠在第j堆上的积木是k,叠的状态(一共三种)是l,此时的最大高度和。
转移一共有三种:
1.不把当前的木块i叠在任一堆上
2.新建一堆,i为最底下的一个
3.把i叠在l上
只有第三个是需要判断的,这个状态表示其实写起来很简单,只是需要想清楚初值边界。





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值