目录
动态规划要点
动态规划的算法设计
1:找出最优解的性质,并描述其结构特征
2:递归定义最优值
3:以自底向上的方式计算最优值
4:根据计算最优值时得到的信息构造出最优解
能采用动态规划求解的问题的一般要具有3个性质
(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3) 有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。
使用动态规划求解问题,最重要的就是确定动态规划三要素
(1)问题的阶段
(2)每个阶段的状态
(3)从前一个阶段转化到后一个阶段之间的递推关系。
动态规划的具体步骤:
(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。
(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。
P1775 石子合并(弱化版)
大区间包含小区间,从最小的区间开始算起,一层一层往上垒。
#include<bits/stdc++.h>
using namespace std;
int a[310],sum[310],dp[310][310],node[310][310];
///a:储存石子数;
//sum:储存前缀和;
//dp:记录每个对每个区间动态规划后的值(第一维左端点,第二维右端点);
//node:标记每个区间动态规划得到最小值的相应分隔点 (最优分隔点下标)
int n;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
dp[i][j]=0x3ffffff;//要求最小值,首先全部初始化为无穷大
for(int i=1;i<=n;i++)
{
cin>>a[i];
sum[i]=sum[i-1]+a[i];//先算出前缀和,避免后面需要反复求和
dp[i][i]=0;//在未合并时没有额外消耗,初始值为0
node[i][i]=i;//对于单个结点形成的区间来说,最优分隔点就是它自己
}
for(int len=1;len<=n;len++)//枚举长度,从底向上
{
for(int j=1;j+len<=n+1;j++)//枚举左端点
{
int ends=j+len-1;//求出右端点位置
for(int i=node[j][ends-1];i<=node[j+1][ends];i++)//四边形不等式,最优分隔点必位于这个区间
{
if(dp[j][ends]>dp[j][i]+dp[i+1][ends]+sum[ends]-sum[j-1])
{
dp[j][ends]=dp[j][i]+dp[i+1][ends]+sum[ends]-sum[j-1];//更新最小值
node[j][ends]=i;//记录对应的结点位置
}
}
}
}
cout<<dp[1][n];//输出最上层的结果
return 0;
}
P1880 [NOI1995] 石子合并
当链变成环之后,起点可以使环上的任意一个点,为了从每个起点处都可以按序遍历到每个结点,可以破环成链,只需要扩充一倍数组大小。
#include<bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f
int dp[205][205],dp2[205][205];
// 最小值 最大值
int sum[205];
int node[205][205];//最小值对应的分隔点
int num[105];
int main()
{
int n;
scanf("%d",&n);
memset(sum,0,sizeof(sum));
memset(dp,0x3f,sizeof(dp));
for(int i=1;i<=n;i++)
{
scanf("%d",&num[i]);
dp[i][i]=0;
node[i][i]=i;
sum[i]=sum[i-1]+num[i];
}
for(int i=1;i<=n;i++)
{
sum[i+n]=sum[i+n-1]+num[i];//延长链
node[i+n][i+n]=i+n;//分隔点初始化
dp[i+n][i+n]=0;
}
for(int len=2;len<=n;len++)
{
for(int j=1;j+len<=2*n;j++)
{
int ends = j+len - 1;
dp2[j][ends]=max(dp2[j+1][ends],dp2[j][ends-1])+sum[ends]-sum[j-1];
/*为什么最大值从可以两个端点的最大者取得?
首先可以把最后一步看成两堆石子合并,倒数第二步看成三堆石子中的两堆合并,再与第三堆合并。
那三堆中哪两堆合并呢?(用w[i]表示第i堆重量)
前两堆:w1=2w[1]+2w[2]+w[3]w1=2w[1]+2w[2]+w[3]
后两堆:w2=w[1]+2w[2]+2w[3]w2=w[1]+2w[2]+2w[3]
所以应该将1号和3号中较大的一堆与第2堆合并,也就是把一堆合并得尽可能大 */
for(int k = node[j][ends-1];k<=node[j+1][ends];k++)
{
if(dp[j][ends]>dp[j][k]+dp[k+1][ends]+sum[ends]-sum[j-1])
{
dp[j][ends]=dp[j][k]+dp[k+1][ends]+sum[ends]-sum[j-1];
node[j][ends] = k;
}
}
}
}
int ans1 = 0xfffffff,ans2 = -1;
for(int i=1;i<=n;i++)//环上的每个结点当做起点形成一个链,看哪条链算出的结果最小(最大)
{
ans1=min(ans1,dp[i][i+n-1]);
ans2=max(ans2,dp2[i][i+n-1]);
}
printf("%d\n%d",ans1,ans2);
return 0;
}
也可以直接枚举[j,ends)之间的分隔点。
P2858 [USACO06FEB]Treats for the Cows G/S
不能用贪心(每次取出两端中较小的),因为每次决策都会影响到后续结果。
仍然考虑最小的区间:最后只剩一份零食的时候,它是被最后取出的,赋初始值的时候需要乘上总天数;让与这份零食相邻的零食进入这个区间,它是前一天取出的,要乘的天数减1,以此类推。
#include<bits/stdc++.h>
using namespace std;
int n,a[2010],dp[2010][2010];
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
dp[i][i]=a[i]*n;
}
for(int len=2;len<=n;len++)
{
for(int l=1;l<=n;l++)
{
int r=l+len-1;
if(r>n) break;
dp[l][r]=max(dp[l+1][r]+a[l]*(n-len+1),dp[l][r-1]+a[r]*(n-len+1));
}
}
cout<<dp[1][n];
return 0;
}
P1063 [NOIP2006 提高组] 能量项链
#include<bits/stdc++.h>
using namespace std;
int a[205],dp[205][205];
int n;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
a[n+i]=a[i];
}
int ans=0;
for(int len=2;len<=n;len++)//枚举区间长度
{
for(int i=1;i+len<=2*n;i++)//枚举左端点
{
int ends=i+len;//左闭右开区间
for(int k=i+1;k<ends;k++)//枚举分隔点
dp[i][ends]=max(dp[i][ends],dp[i][k]+dp[k][ends]+a[i]*a[k]*a[ends]);
//状态转移方程:max(原来能量,左区间能量+右区间能量+合并后生成能量)
}
}
for(int i=1;i<=n;i++)
ans=max(ans,dp[i][n+i]);
cout<<ans;
return 0;
}
3146 [USACO16OPEN]248 G
#include<bits/stdc++.h>
using namespace std;
int dp[250][250],ans;
//dp:记录每个对每个区间动态规划后的值(第一维左端点,第二维右端点);
int n;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
cin>>dp[i][i];
for(int len=1;len<n;len++)//枚举长度,从底向上
{
for(int j=1;j+len<=n;j++)//枚举左端点
{
int ends=j+len;//求出右端点位置
for(int i=j;i<ends;i++)
{
if(dp[j][i]==dp[i+1][ends])
{
dp[j][ends]=max(dp[j][ends],dp[j][i]+1);
ans=max(ans,dp[j][ends]);
}
}
}
}
cout<<ans;
return 0;
}
P3147 [USACO16OPEN]262144 P
数据范围在上一题基础上加强,再枚举两个坐标已经不可行了,不妨把原来的最大值和区间右端坐标互换一下位置,即 f [ i ] [ j ] 表示以 i 为起点,值为 j 的区间的右端点坐标。
这样就可以大大的缩减时间复杂度:O(58*n)。
#include<bits/stdc++.h>
using namespace std;
int a[300000],f[300000][60];
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
f[i][a[i]]=i;//i为区间左端点, f[i][a[i]]的值为区间右端点,a[i]为这个区间储存的值
}
int ans=0;
for(int k=2;k<=58;k++)
{
for(int i=1;i<=n;i++)
{
if(f[i][k-1]&&f[f[i][k-1]+1][k-1])//两个相邻的区间,它们的值都是k-1
{
f[i][k]=f[f[i][k-1]+1][k-1];//可以合并成值为k的区间
ans=max(ans,k);
}
}
}
printf("%d\n",ans);
return 0;
}
P4170 [CQOI2007]涂色
#include<bits/stdc++.h>
using namespace std;
int dp[55][55];
string s;
int main()
{
cin>>s;
int len=s.size();
for(int i=0;i<len;i++)
for(int j=0;j<len;j++)
dp[i][j]=0x3ffffff;//因为要求最小值,所以先初始化为无穷大
for(int i=0;i<len;i++)
dp[i][i]=1;//对于单个点,涂色的最小次数为1(初始化)
for(int k=1;k<len;k++)//枚举长度
{
for(int l=0;l+k<len;l++)//枚举左端点
{
int r=l+k;//求出右端点
if(s[l]==s[r])
dp[l][r]=dp[l][r-1];//右端点可以直接用左端点的底色,实际用的颜色数和不包括右端点的情况相同
else for(int t=l;t<r;t++)//枚举每一个隔断点
dp[l][r]=min(dp[l][r],dp[l][t]+dp[t+1][r]);//找到该区间所需要的最少颜色数
}
}
cout<<dp[0][len-1];
return 0;
}
最长对称子串
对给定的字符串,本题要求你输出最长对称子串的长度。例如,给定Is PAT&TAP symmetric?,最长对称子串为s PAT&TAP s,于是你应该输出11。
输入格式:
输入在一行中给出长度不超过1000的非空字符串。
输出格式:
在一行中输出最长对称子串的长度。
输入样例:
Is PAT&TAP symmetric?
输出样例:
11
状态转移方程:
if(s[i]==s[j]) dp[i][j]=dp[i+1][j-1];
else dp[i][j]=0;
#include <bits/stdc++.h>
using namespace std;
const int maxn=1010;
int dp[maxn][maxn] ;
int main()
{
string s;
getline(cin,s);
int len=s.size();
int ans=1;
for(int i=0;i<len;i++)
{
dp[i][i]=1;
if(i<len-1)
{
if(s[i]==s[i+1])
{
dp[i][i+1]=1;
ans=2;
}
}
}
for(int L=3;L<=len;L++)//枚举子串的长度
{
for(int i=0;i+L-1<len;i++)//枚举子串的起始端点
{
int j=i+L-1;//子串右端点
if(s[i]==s[j]&&dp[i+1][j-1]==1)
{
dp[i][j]=1;
ans=L;//更新回文子串长度
}
}
}
cout<<ans;
return 0;
}