【算法详解】简单区间动态规划

区间动态规划的含义与模板解释

区间DP,其实求的就是一个区间内的最优值.
一般这种题目,在设置状态的时候,都可以设f[i][j]为区间i-j的最优值
而f[i][j]的最优值,这有两个小区间合并而来的,为了划分这两个更小的区间,我们则需用用一个循环变量k来枚举,而一般的状态转移方程便是:

f [ i ] [ j ] = m a x / m i n ( f [ i ] [ j ] , f [ i ] [ k ] + f [ k ] [ j ] + s o m e t h i n g )
我们则需要根据这个题目的实际含义进行变通即可.
而区间dp的大致模板是:

for (int len=2;len<=n;len++)
    for (int i=1;i+len-1<=n;i++)
    {
        int j=i+len-1;
        for (int k=i;k<=j;k++)
            f[i][j]=max/min(f[i][j],f[i][k]+f[k][j]+something)
    }

len枚举区间的长度,i和j分别是区间的起点和终点,k的作用是用来划分区间.


石子合并

题目描述
在操场上沿一直线排列着n堆石子。现要将石子有次序地合并成一堆。
规定每次只能选相邻的两堆石子合并成新的一堆,并将新的一堆石子数计为该次合并的得分。 我们希望这n-1次合并后得到的得分总和最小。
输入格式
第一行有一个正整数n(n<=300),表示石子的堆数; 第二行有n个正整数,表示每一堆石子的石子数,每两个数之间用一个空格隔开。它们都不大于10000。
输出格式
一行,一个整数,表示答案。
样例数据
input
3
1 2 9
output
15

这道题其实就是一个区间DP的一个模板,即每次进行合并的时候就是合并一个区间.
我们可以设f[i][j]为合并i-j区间的石子的最优方案即最小花费,假设在我们进行i和j的状态转移的时候必然更小的区间已经求出了最优的方案数,那么我们只需要用k去划分石子数去进行状态转移即可.
因为本来的时间是O(n^3),我们则需要优化时间,采用前缀和的方法,把合并的数量进行计算即可.
我们可以得到状态转移方程:

f [ i ] [ j ] = m i n ( f [ i ] [ j ] , f [ i ] [ k ] + f [ k + 1 ] [ j ] + c o s t [ i ] [ j ] )

表示两堆石子的合并花费加上新的花费得到的结果.
在代码实现上,我们需要注意:len是从2开始的,诸如f[1][1],f[2][2]都是不需要花费价值的,即初始值0;状态转移方程是f[i][k]+f[k+1][j]+cost[i][j]) 而不是f[i][k]+f[k][j]+cost[i][j])合并的是两堆不同的石子而不是一堆,这点需要注意.
代码如下:

#include<bits/stdc++.h>
using namespace std;
int n;
int f[1000][1000]={};
int sum[1000]={};
int a[1000]={};
int cost[1000][1000]={};
int main()
{
    cin>>n;
    for (int i=1;i<=n;i++)
    {
        cin>>a[i];
        sum[i]=sum[i-1]+a[i];
    }
    for (int i=1;i<=n;i++)
        for (int j=i;j<=n;j++)
            cost[i][j]=sum[j]-sum[i-1];
    for (int len=2;len<=n;len++)
        for (int i=1;i<=n;i++)
        {
            int j=i+len-1;
            if (j>n) continue;
            for (int k=i;k<j;k++)
                if ((f[i][k]+f[k+1][j]+cost[i][j]<f[i][j])||!f[i][j])
                    f[i][j]=f[i][k]+f[k+1][j]+cost[i][j];
        }
    cout<<f[1][n];
    return 0;
}

矩阵连乘

题目描述
给定 n个矩阵{A1,A2,…,An},考察这 n个矩阵的连乘积 A1A2…An。由于矩阵乘法满足结合律,故计算矩阵的连乘积可以有许多不同的计算次序,这种计算次序可以用加括号的方式来确定。
矩阵连乘积的计算次序与其计算量有密切关系。例如,考察计算 3 个矩阵{A1,A2,A3}连乘积的例子。 设这3个矩阵的维数分别为10∗100,100∗5,和5∗50
若按(A1A2)A3 计算,3 个矩阵连乘积需要的数乘次数为 10∗100∗5+10∗5∗50=7500
若按 A1(A2A3)计算,则总共需要100∗5∗50+10∗100∗50=75000 次数乘。
现在你的任务是给出一个矩阵连乘式,计算其需要的最少乘法次数。
输入格式
输入数据由多组数据组成。每组数据格式如下: 第一行是一个整数n(1≤n≤100),表示矩阵的个数。 接下来 n 行,每行两个整数 a,b,分别表示该矩阵的行数和列数,其中1 < a,b < 100。
输出格式
对于每组数据,输出仅一行包含一个整数,即将该矩阵连乘方案需要的最少乘法次数。
样例数据
input
3
10 100
100 5
5 50
output
7500

这道题目,其实也是区间动态规划的模板题.同石子合并,两个区间想乘即为上述题目两个石子堆的合并,那么最后求的数必然就相当于一堆石子合并起来的花费,只需要在状态转移方程上面略加改动即可.有2个状态转移方程,选择每一种进行转移都能正确:

f [ i ] [ j ] = m i n ( f [ i ] [ j ] , f [ i ] [ k ] + f [ k + 1 ] [ j ] + a [ i ] b [ k ] b [ j ] )

f [ i ] [ j ] = m i n ( f [ i ] [ j ] , f [ i ] [ k ] + f [ k + 1 ] [ j ] + a [ i ] a [ k + 1 ] b [ j ] )

代码如下:

#include<bits/stdc++.h>
using namespace std;
int n;
int a[200];
int b[200];
int f[200][200];
int main()
{
    cin>>n;
    for (int i=1;i<=n;i++)
        cin>>a[i]>>b[i];
    for (int len=2;len<=n;len++)
        for (int i=1;i+len-1<=n;i++)
        {
            int j=i+len-1;f[i][j]=pow(10,9);
            for (int k=i;k<j;k++)
                f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+a[i]*a[k+1]*b[j]);    
        } 
    cout<<f[1][n]<<endl;
    return 0;
}

能量项链【noip2006提高组】

【问题描述】原题题面戳这里

这道题目比较复杂,因为这是一个环状的区间动态规划,即并不是线性,只要在n的后面再将原来的序列复制一遍即可.即我们输入的是t[1]-t[n],只需要添加一句t[i+n]=t[i]即可.因为首尾可以相连,所以我们其实做的是多个矩阵连乘,即以不同的起点做一遍i到j即可,在最后输出的时候就不只是f[i][n]了,而是:

a n s = m a x ( f [ i ] [ i + n ] ) i 1 n

代码如下:

#include<bits/stdc++.h>
using namespace std;
int n,ans=0;
int t[10000];
int a[10000];
int b[10000];
int f[8000][8000];
int main()
{
    cin>>n;
    for (int i=1;i<=n;i++)
        cin>>t[i],t[i+n]=t[i];
    for (int i=1;i<=2*n;i++)
        a[i]=t[i],b[i]=t[i+1]; 
    for (int len=1;len<=2*n;len++)
        for (int i=1;i+len-1<=n*2;i++)
        {
            int j=i+len-1;
            for (int k=i;k<j;k++)
                f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+a[i]*b[k]*b[j]);
        }
    for (int i=1;i<=n;i++)
        ans=max(ans,f[i][i+n-1]);
    cout<<ans<<endl;
    return 0;
}

关灯

题目描述
宁智贤得到了一份有趣而高薪的工作。每天早晨她必须关掉她所在村庄的街灯。所有的街灯都被设置在一条直路的同一侧。 宁智贤每晚到早晨5点钟都在晚会上,然后她开始关灯。开始时,她站在某一盏路灯的旁边。 每盏灯都有一个给定功率的电灯泡,因为宁智贤有着自觉的节能意识,她希望在耗能总数最少的情况下将所有的灯关掉。 宁智贤因为太累了,所以只能以1m/s的速度行走。关灯不需要花费额外的时间,因为当她通过时就能将灯关掉。 编写程序,计算在给定路灯设置,灯泡功率以及宁智贤的起始位置的情况下关掉所有的灯需耗费的最小能量。
输入格式
第一行包含一个整数N,2≤N≤1000,表示该村庄路灯的数量。
第二行包含一个整数V,1≤V≤N,表示宁智贤开始关灯的路灯号码。
接下来的N行中,每行包含两个用空格隔开的整数D和W,用来描述每盏灯的参数,其中0≤D≤1000,0≤W≤1000。
D表示该路灯与村庄开始处的距离(用米为单位来表示),W表示灯泡的功率,即在每秒种该灯泡所消耗的能量数。路灯是按顺序给定的。
输出格式
第一行即唯一的一行应包含一个整数,即消耗能量之和的最小值。注意结果小超过1,000,000,000。
样例数据
input
4
3
2 2
5 8
6 1
8 7
output
56

显然,这道题目同样也是区间动态规划,即我们无法用贪心(以往一边关到底再回来这一策略)显然不可行,若使用暴搜则同样可以拿到部分分.因此,我们考虑使用区间动态规划来解决这个问题.我们可以设f[i][j]为关掉区间i到区间j的所有灯的最小消耗数目,我们不难得知:
f[i][j]这个状态的转移只和f[i+1][j]与f[i][j-1]有关,因为我们知道,要使一个区间内的状态最优便必然要关完一个区间内的等必然结束在该区间的两端,因此我们f[i][j]在状态结束的时候必然结尾在位置i和位置j,那么必然与位置i和位置j旁边的点相关.
为了能够分清状态的转移与每一个结束点位置的关系,我们可以选择再增加一维地状态,即:
1.设f[i][j][0]表示区间i-j且最后停留在左边的最小耗费
2.设f[i][j][1]表示巨剑i-j且最后停留在右边的最小耗费
跟据上述两个状态,我们便可以得出显然性的结论:f[i][j][0]必然是由f[i+1][j][0/1]转移过来的,f[i][j][1]必然是由f[i][j-1][0/1]转移过来的,那么我们便可以得到状态转移方程:

f [ i ] [ j ] [ 0 ] = m a x ( f [ i ] [ j ] [ 0 ] , f [ i + 1 ] [ j ] [ 0 ] + s u m ( i , i + 1 , i + 1 , j ) , f [ i + 1 ] [ j ] [ 1 ] + s u m ( i , j , i + 1 , j ) )

f [ i ] [ j ] [ 1 ] = m a x ( f [ i ] [ j ] [ 1 ] , f [ i ] [ j 1 ] [ 1 ] + s u m ( j 1 , j , i , j 1 ) , f [ i ] [ j 1 ] [ 0 ] + s u m ( i , j , i , j 1 ) )

s u m ( P o s 1 , P o s 2 , L e f t , R i g h t ) = | D [ P o s 2 ] D [ P o s 1 ] | ( S [ n ] S [ R i g h t ] + S [ L e f t ] )

1.函数sum的含义:表示这个人从位置Pos1(或Pos2)走到位置Pos2或(Pos1)的位置时除了Left到Right区间外的灯耗费的电费,两者被染相乘.D表示位置,在体面上便有所说明;而S则是前缀和的意思,即S[i]=w[1…i]
2.状态转移方程的含义:表示上一个阶段的花费加上走过来时灯的花费的最小值.
因此应该很好理解了吧!
代码如下:

#include<bits/stdc++.h>
using namespace std;
int N,V;
int D[100000];
int W[100000];
int s[100000];
int f[3000][3000][2];
inline int sum(int p1,int p2,int d1,int d2)//分别表示从走的距离和已经关了灯的区间编号,则消耗的量为距离*区间之外的等的消耗总和 
{   return abs(D[p1]-D[p2])*(s[N]-s[d2]+s[d1-1]);  }
inline int Min(int a,int b,int c)
{   return min(a,min(b,c));  } 
int main()
{
    cin>>N>>V;
    for (int i=1;i<=N;i++)
    {
        cin>>D[i]>>W[i];
        s[i]=s[i-1]+W[i];
    }
    memset(f,100,sizeof(f));
    f[V][V][0]=f[V][V][1]=0;//0在左边,1在右边 
    for (int len=2;len<=N;len++)
        for (int i=1;i+len-1<=N;i++)
        {
            int j=i+len-1;
            f[i][j][0]=Min(f[i][j][1],f[i+1][j][0]+sum(i,i+1,i+1,j),f[i+1][j][1]+sum(i,j,i+1,j));
            f[i][j][1]=Min(f[i][j][1],f[i][j-1][1]+sum(j-1,j,i,j-1),f[i][j-1][0]+sum(i,j,i,j-1));
        }
    cout<<min(f[1][N][1],f[1][N][0]);
    return 0;
} 

猜你喜欢

转载自blog.csdn.net/ronaldo7_zyb/article/details/81087952