动态规划
介绍
一个问题必须有重叠子问题和最优子结构,才能使用动态规划(最优子结构:一个问题的最优解可以由其子问题的最优解有效构造)
-
分治与动态规划
都是将问题分为子问题,合并子问题的解得到原问题的解。但是分治法解出的子问题是不重叠的(如归并排序和快速排序是分别处理左右序列,不会出现重叠),且分治法解决的问题不一定是最优化问题 -
贪心与动态规划
都要求原问题必须拥有最优子结构。但是贪心算法采用“自顶向下”,直接选择一种子问题求解,没被选择的子问题就不去了解(单链的流水方式)。而动态规划是从边界开始向上得到目标解,会考虑所有子问题。
最大连续子序列和P430
问题描述:序列A1,A2,…,An中,求i,j,使得Ai+…+Aj最大。
解决:设计dp数组,dp[i]表示以A[i]作为结尾的连续序列最大和。
状态转移方程:dp[i] = max{A[i],dp[i-1]+A[i]}
边界:dp[0] = A[0]
状态的无后效性:历史信息只能通过已有的状态去影响未来的决策。如每次计算状态dp[i],只会涉及到dp[i-1],不直接用到dp[i-1]中的历史信息。
int dp[maxn],a[maxn],s[maxn]={0};//s表示dp[i]是从那个元素开始的
dp[0]=a[0];
for(int i=1;i<n;i++){
if(dp[i-1]+a[i]>a[i]){
dp[i]=dp[i-1]+a[i];
s[i]=s[i-1];
}else{
dp[i]=a[i];
s[i]=i;
}
}
最长不下降子序列LIS P432
问题描述:寻找最短子序列(可以不连续),使得子序列不下降(非递增)
解决:dp数组表示序列的程度,如果A[i]之前存在A[j]使得A[j]<=A[i]且dp[j]+1>dp[i]就可以把A[i]跟在以A[j]结尾的LIS后面。dp[i] = dp[j]+1
状态转移方程:dp[i] = max{1,dp[j]+1} (j=1,2,...,i-1&&A[j]<A[i])
边界:dp[i]=1(1<=i<=n)
int ans=-1;//不下降子序列的长度
for(int i=0;i<num;i++){
dp[i]=1;
for(int j=0;j<i;j++){
if(A[j]<=A[i]&&dp[i]<dp[j]+1){
dp[i]=dp[j]+1;
}
}
ans=max(ans,dp[i]);
}
最长公共子序列LCS P434
问题描述:求两个字符串A,B的最长公共部分(可以不连续)
解决:(字符串下标从1开始) dp[i][j]表示字符串A的i号位和字符串B的j号位之前的LCS长度
状态转移方程:
当 A[i] == B[j]
时,有 dp[i][j]=dp[i-1][j-1]+1;
当 A[i] != B[j]
时,有 dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
边界:dp[i][0]=dp[0][j]=0 (0<=i<=n,0<=j<=m)
当允许重复元素时,状态转移方程:
当 A[i] == B[j]
时,有 dp[i][j]=max(dp[i-1][j],dp[i][j-1]) + 1;
当 A[i] != B[j]
时,有 dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
最长回文子串 P436
问题描述:求字符串S中的最长回文串长度
解决:dp[i][j]表示S[i]至S[j]所表示的字串是否是回文串,是则为1,不是则为0.
状态转移方程:
当S[i] == S[j]
,有:dp[i][j]=dp[i+1][j-1]
当S[i] != S[j]
,有:dp[i][j]=0
边界:dp[i][i]=1, dp[i][i+1] = (S[i]==S[i+1])?1:0
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;//更新最长回文串长度 }
}
}
DAG(有向无环图)最长路 P439
- 求整个DAG中的路径(不固定起点和终点)
可以令dp[i]表示从i号顶点出发能获得的最大路径长度,有dp[i] = max{dp[j]+length[i->j]|(i,j∈E)}
用choice表示最长路径上的后继节点初始值为-1(这里是从后向前计算,可以直接求出最小字典序的最长路径)
边界:出度为0的顶点的dp值为0,具体实现可以令整个dp初始值为0
int DP(int i){
if(dp[i]>0) return dp[i];
for(int j=0;j<n;j++)
if(G[i][j]!=INF){
int temp=DP(j)+G[i][j];
if(temp>dp[i]){//可以获得更长路径
dp[i]=temp; choice[i]=j;//i的后继是j
}
}
return dp[i];//遇见出度为0的顶点
}
//首先得到最大的dp[i],将i作为路径起点
void output(int i){
printf("%d",i);
while(choice[i]!=-1){//choice的初始值为-1
i=choice[i];
printf("->%d",i);
}
}
- 固定终点,求DAG的最长路径长度
设置边界为dp[T]=0,其余的dp值为-INF,设置vis数组表示顶点已经被计算
int DP(int i){//dp[T]=0,vis[T]=true;
if(vis[i]) return dp[i];//dp已经被计算
vis[i]=true;
for(int j=0;j<n;j++)
if(G[i][j]!=INF)
dp[i]=max(dp[i],DP(j)+G[i][j]);
return dp[i];
}
背包问题
01背包问题 P443
问题描述:n件物品,重量w[i],价值c[i],背包容量V,求可以得到的最大价值。每个物品只有一件。
dp[i][v]表示前i件物品恰好装入容量为v的背包中获得的最大价值。
状态转移方程:dp[i][v] = max{dp[i-1][v],dp[i-1][v-w[i]]+c[i]} (1<=i<=n,w[i]<=v<=V)
(枚举方向正序)
边界:dp[0][v]=0(0<=v<=V)
优化空间复杂度:dp[v] = max(dp[v],dp[v-w[i]]+c[i]) (1<=i<=n,w[i]<=v<=V)
(枚举方向i正序,v逆序),边界:dp初始化0
for(int i=1;i<=n;i++)
for(int v=V;v>=w[i];v--)
dp[v]=max(dp[v],dp[v-w[i]]+c[i]);
注:对于如何输出序列,可以参照实验书中P387,使用choice[i][w]表示选择的策略。
完全背包问题
问题描述:n件物品,重量w[i],价值c[i],背包容量V,求可以得到的最大价值。每个物品可以有无数件。
状态转移方程:dp[i][v] = max{dp[i-1][v],dp[i][v-w[i]]+c[i]} (1<=i<=n,w[i]<=v<=V)
边界:dp[0][v]=0(0<=v<=V)
优化空间复杂度:dp[v] = max(dp[v],dp[v-w[i]]+c[i]) (1<=i<=n,w[i]<=v<=V)
(v是正向枚举),边界dp初始化0
for(int i=1;i<=n;i++)
for(int v=w[i];v<=V;v++)
dp[v]=max(dp[v],dp[v-w[i]]+c[i]);
字符串
字符串hash
hash函数:H[i] = (H[i-1]*p + index(str[i])) % mod
其中mod=1e9+7
,p=1e7+19
子串str[i…j]的hash值为:H[i...j] = ((H[i]-H[i-1]*p^(j-i+1)) % mod + mod) % mod
相关函数:
typedef long long LL;
const LL MOD=1e9+7;
const LL P=1e7+19;
const LL maxn=1010;
LL powP[maxn],h1[maxn],h2[maxn];
vector<pair<int,int> > pr1,pr2;//<子串hash值,子串长度>
void inti(int len){//powP[i]存放p^i%MOD
powP[0]=1;
for(int i=1;i<len;i++)
powP[i]=(powP[i-1]*P)%MOD;
}
void calH(LL H[],string &str){//计算hsah
H[0]=str[0];
for(int i=1;i<str.length();i++)
H[i]=(H[i-1]*P+str[i])%MOD;
}
int calSingleSubH(LL H[],int i,int j){//计算H[i...j]
if(i==0) return H[j];
return ((H[j]-H[i-1]*powP[j-i+1])%MOD+MOD)%MOD;
}
- 求最长公共子串的长度(子串连续) P451
- 二分法求最长回文子串 P453
str1反转后得到str2,str1的[a, b]对应str2的[len-1-b, len-1-a]
回文串的长度是奇数时:回文串中点i,半径k。判断str1中[i-k, i]和[i, i+k]是否是反转串,等价与判断str1中[i-k, i]和str2中[len-1-(i+k), len-1-i]是否等价
回文串的长度是偶数时:回文串中间空隙第一个元素下标i,半径k。判断str1中[i-k+1, i]和[i+1, i+k]是否是反转串,等价与判断str1中[i-k+1, i]和str2中[len-1-(i+k), len-1-(i+1)]是否等价
//二分法求解,寻找最后一个满足hashL==hashR的半径,也就是:寻找第一个hashL!=hashR的回文半径,然后-1
//isEven表示奇回文串为0,偶回文串为1
int binarySearch(int l,int r,int len,int i,int isEven){
while(l<r){//len表示字符长度
int mid=(l+r)/2;//回文半径为mid,中点为i
int H1L=i-mid+isEven, H1R=i;
int H2L=len-1-(i+mid), H2R=len-1-(i+isEven);
int hashL=calSingleSubH(H1,H1L,H1R);
int hashR=calSingleSubH(H2,H2L,H2R);
if(hashL!=hashR) r=mid;//满足条件,r=mid
else l=mid+1;
}
return l-1;
}
int main(){
inti();
string str; getline(cin,str);
calH(H1,str);
reverse(str.begin(),str.end());
calH(H2,str);
int ans=0;
//奇回文
for(int i=0;i<str.length();i++){//i为中点
int maxLen=min(i, (int)str.length-1-i)+1;//最长半径
int k=binarySearch(0,maxLen,str.length(),i,0);//最长回文半径
ans=max(ans,k*2+1);
}
//偶回文
for(int i=0;i<str.length();i++){//i为中点
int maxLen=min(i+1, (int)str.length-1-i)+1;//最长半径(注意左长为i+1)
int k=binarySearch(0,maxLen,str.length(),i,1);//最长回文半径
ans=max(ans,k*2);
}
return 0;
}
KMP算法
next[i]是字串s[0…1]最长相等前后缀的前缀最后一位下标。(0为开始下标)
next数组的计算
void getNext(char s[],int len){
int j=-1;
next[0]=-1;
for(int i=1;i<len;i++){
while(j!=-1&&s[i]!=s[j+1]) j=next[i];//直到j退回到-1或者相等的位置
if(s[i]==s[j+1]) j++;
next[i]=j;
}
}
KMP算法
int KMP(char text[],char patern[]){
int n=strlen(text),m=strlen(pattern);
getNext(pattern,m);
int ans=0,j=-1;
for(int i=0;i<n;i++){
while(j!=-1&&text[i]!=pattern[j+1]) j=next[j];
if(text[i]==pattern[j+1]) j++;
if(j==m-1){
ans++;
j=next[j];
}
}
return ans;
}
改进next数组:nextval[i]是模式串pattern的i+1为失配时,i应当退回的最佳位置。
void getNextval(char s[],int len){
int j=-1;
nextval[0]=-1;
for(int i=1;i<len;i++){
while(j!=-1&&s[i]!=s[j+1]) j=nextval[i];
if(s[i]==s[j+1]) j++;
//下面算法与getNext不同
if(j==-1||s[i+1]!=s[j+1]) nextval[i]=j;
else nextval[i]=nextval[j];
}
}
专题拓展
分块思想
问题描述:在随时添加或删除元素的情况下,实时实时查询序列中第k大的元素(在线查询)
将序列分块,每块大小n=N1/2(向下取整),设置table数组记录元素的个数,block数组表示第i块的元素个数。
添加元素x,让table[x]和block[x/n]加一
void find(int k){
int sum=0, index=0;
while(sum+block[index]<k)
sum+=block[index++];
int num=index*sqrN;
while(sum+table[num]<k)
sum+=table[num++];
printf("%d\n",num);
}
树状数组BIT(下标从1开始)
lowbit(x) = x & (-x),表示能整除x的最大2的幂次
下面令c[i]表示在i号元素之前(含i号元素)lowbit(i)个整数之和。
#define lowbit(i) ((i)&(-i))
int c[maxn];
void updata(int x,int v){//第x和元素加上v
for(int i=x;i<maxn;i+=lowbit(i))
c[i]+=v;
}
int getSum(int x){//计算前x个整数的和
int sum=0;
for(int i=x;i>0;i-=lowbit(i))
sum+=c[i];
return sum;
}
对于寻找序列中第k大的元素,就是寻找第一个满足getSum(i)>=k的i