【算法与数据结构】——区间DP

介绍

区间DP属于线性DP的一种,以区间长度作为DP的阶段,以区间的左右端点作为状态的维度。一个状态通常由被它包含且比它更小的区间状态转移而来。阶段(长度),状态(左右端点),决策三者按照由外到内的顺序构成3层循环。
接下来介绍一个典型问题。

石子合并问题

有N堆石子排成一排,每堆石子有一定的数量。先要将2堆石子合并成一堆,每次合并花费的代价为这两堆石子的和,经过N-1次合并后称为一堆。求总的代价最小值。
问题分析
1.求某个区间的最优解[1,n];
2.大的区间由包含于它的小区间组成(转移而来);
3.满足DP的三个基本条件。
状态
通用状态的定义:DP[i][j]从i到j区间的最优解。
目标状态的表示:DP[1,n]
阶段的划分:区间长度
决策(状态转移)
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+w[i][j])
w[i][j]是从i到j区间总的石子重量,可以用前缀和求得。
**区间DP:**小结:在区间上动态规划,具体的说就是,现在小区间进行DP得到小区间的最优解,然后通过合并小区间的最优解,进而求出大区间的最优解。
阶段的划分:区间长度
状态的表示:枚举起点(不同起点,不同状态)
决策的实现:枚举分割点

区间DP的状态设计相对简单,基本大部分问题都适用。
DP[i][j]代表区间[i,j]上的最优解。
状态转移一定是从区间长度短的状态到区间长度长的状态。
模板
迭代,时间复杂度 ( O ( n 3 ) ) (O(n^3)) (O(n3))

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]+w[i][j];
		}
	}
}

记忆化DFS:时间复杂度 ( O ( n 2 ) ) (O(n^2)) (O(n2))

int MINdfs(int l,int r)		//推荐记忆化DFS的实现方式,更简单,不容易出错。
{
	int &D = dp[l][r];	//引用D就代表dp[l][r],这个方法可以简化代码
	//inf是自定义的整型最大值
	if(D!=inf)return D;	//如果已经搜索过就返回这个值,记忆化搜索的重点
	if(l == r) return D=0;	//如果l==r,无需合并,所以返回0
	for(int i = l;i<r;i++)	//枚举所有可行的区间分割方案
		D=min(D,MINdfs(l,i)+MINdfs(i+1,r)+sum[r]-sum[l-1];
	return D;	
}

回文

题目Cheapest Palindrome
题意:给出一个字符串,以及添加或删除部分字符的成本,求将字符串变为回文串所需的最小成本。只能添加或删除给出成本的字符。
思路:区间DP,用dp[i][j]表示将字符串下标i到j的子串变为回文串所需的最小成本,无非是在左端删除一个字符或者在右端添加一个字符。两种情况,如果s[i]等于s[j]就不需要进行操作,dp[i][j] = dp[i+1][j-1],如果s[i]不等于s[j],要么在右端添加字符s[i]要么在左端删除字符s[i],两种情况取最小值dp[i][j] = min(dp[i+1][j]+w[str[i]-‘a’],dp[i][j-1]+w[str[j]-‘a’])
AC代码:

/*
**Author:skj
**Time:8-6
**Function:区间DP poj3280
*/
//#include <bits/stdc++.h>
#include <string>
#include <iostream>
#define INF 0x3f3f3f3f
using namespace std;
const int maxm = 2002;

int dp[maxm][maxm];
int w[27];
int main()
{
	freopen("in.txt","r",stdin);
	freopen("out.txt","w",stdout);
	cin.sync_with_stdio(0);
	cin.tie(0);
    string str;
    int n,m;
    cin >> n >> m;
    cin >> str;
    while(n--)
    {
        char c;
        int a,b;
        cin >> c;
        cin >> a >> b;
        w[c-'a'] = min(a,b);//因为对于一个字符的操作不是删除就是添加,而且不需要知道最终的字符串,所以到底是删除了字符还是添加了字符就不需要知道的很确切,这里只需要保存一个最小值就可以了
    }
    for(int i = m-1;i >= 0;i--)//从字符串最右侧开始是为了确保能正确遍历dp数组
    {
        for(int j = i+1;j < m;j++)
        {
            if(str[i]==str[j])
            {
                dp[i][j] = dp[i+1][j-1];
            }
            else
            {
                dp[i][j] = min(dp[i+1][j]+w[str[i]-'a'],dp[i][j-1]+w[str[j]-'a']);
            }
        }
    }
    cout << dp[0][m-1] << endl;
    return 0;
}

括号匹配

题目地址POJ 2955 Brackets
题意:正则括号序列,就是从语法上来说合法的括号序列,类似(),()[(())]都可以,但是[(])不可以,给出一个序列,问他的子序列中最长的正则括号序列长度
思路:区间DP,与回文字符串思路基本一样,但是不同的是[(([]))]这种情况,需要处理,在代码中说明。
AC代码:

/*
**Author:skj
**Time:8-7
**Function:poj 2955 区间DP 括号匹配
*/
//#include <bits/stdc++.h>
#include <iostream>
#include <string>
#include <cstring>
#define INF 0x3f3f3f3f
using namespace std;
const int maxn = 101;
int dp[maxn][maxn];
bool judge(char c1,char c2)
{
    if(c1 == '('&&c2==')')
    {
        return true;
    }
    else if(c1=='['&&c2==']')
    {
        return true;
    }
    else if(c1=='{'&&c2=='}')
    {
        return true;
    }
    return false;
}
int main()
{
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
    cin.sync_with_stdio(0);
	cin.tie(0);
	string s;
	cin >> s;
	while(s!="end")
    {
        int l = s.size();
        for(int i = l-1;i>=0;i--)
        {
            for(int j = i+1;j<l;j++)
            {
                if(judge(s[i],s[j]))
                {
                    dp[i][j] = dp[i+1][j-1]+2;
                }
                for(int k = i;k<j;k++)//处理[()]([()])这种情况,需要遍历所有子问题之和取最大值。
                {
                    dp[i][j] = max(dp[i][j],dp[i][k]+dp[k+1][j]);
                }
            }
        }
        cout << dp[0][l-1] << endl;
        memset(dp,0,sizeof(dp));
        cin >> s;
    }
    return 0;
}

猴子派对

题目地址【HDU 3506 Monkey Party
题意:N只猴子坐成环,都不认识对方,森林之王每次介绍两只猴子A和B,A和B相邻,A认识的每只猴子都将认识B已经认识的每只猴子,介绍的总时间是A和B已经认识的所有猴子交友时间的总和。每只猴子都认识自己。求最短的介绍时间。
思路:跟合并石子的题目有点像,属于环型的石子合并。
属于区间DP,n只猴子坐在一个圈里,是一个环,对于包含n个元素的环型,可以将前n-1个元素一次复制到第n个元素后面,将环型转化为直线型。
a1,a2,··· a n a_n an,a1··· a n − 1 a_{n-1} an1,然后以长度作为阶段,以序列的开始和结束下标作为状态的维度,通过不同的情况执行不同的决策。
状态表示:dp[i][j]表示[i,j]区间猴子相互认识的最少时间。枚举每一个位置k,求两个子问题之和的最小值。
状态转移方程:dp[i][j]=min(dp[i][k]+dp[k+1][j]+sum(i,j))。
最后从规模是n的最优值中找出最小值即可。
该算法的时间复杂度为 O ( n 3 ) O(n^3) O(n3),数据范围 1 ≤ n ≤ 1000 1\le n \le1000 1n1000,超时,考虑采用四边不等式优化。
求解dp[i][j]时,需要枚举位置k=i,···,j-1,用s[i][j]记录dp[i][j]取得最小值的位置k。利用四边形不等式优化后,k的枚举范围变为s[i][j-1]~s[i+1][j],枚举范围小了很多。

#include<bits/stdc++.h>
using namespace std;
int a[2010];//存前缀和
int dp[2010][2010];
int s[2010][2010];//记录最优分割点
int inf=0x3f3f3f3f;

int main(){
	int n;
	while(cin>>n){
		for(int i=1;i<=n;i++){
			cin>>a[i];
			a[i+n]=a[i];
		}
		memset(dp,inf,sizeof(dp));
		for(int i=1;i<=n*2;i++) a[i]+=a[i-1];
		for(int i=1;i<=n*2;i++){
			dp[i][i]=0;
			s[i][i]=i;
		}
		for(int len=1;len<n;len++){
			for(int i=1;i<=n;i++){
				int j=i+len;
				for(int k=s[i][j-1];k<=s[i+1][j];k++){
					if(dp[i][k]+dp[k+1][j]+a[j]-a[i-1]<dp[i][j]){
						dp[i][j]=dp[i][k]+dp[k+1][j]+a[j]-a[i-1];
						s[i][j]=k;
					}
				}
			}
		}
		int m=1e9;
		for(int i=1;i<=n;i++){
			m=min(m,dp[i][i+n-1]);
		}
		cout<<m<<endl;
	}
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值