算法笔记4——动态规划、字符串专题、专题拓展

动态规划

介绍

一个问题必须有重叠子问题和最优子结构,才能使用动态规划(最优子结构:一个问题的最优解可以由其子问题的最优解有效构造)

  • 分治与动态规划
    都是将问题分为子问题,合并子问题的解得到原问题的解。但是分治法解出的子问题是不重叠的(如归并排序和快速排序是分别处理左右序列,不会出现重叠),且分治法解决的问题不一定是最优化问题

  • 贪心与动态规划
    都要求原问题必须拥有最优子结构。但是贪心算法采用“自顶向下”,直接选择一种子问题求解,没被选择的子问题就不去了解(单链的流水方式)。而动态规划是从边界开始向上得到目标解,会考虑所有子问题。

最大连续子序列和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+7p=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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值