【算法题】机试指南-动态规划篇

本文仅供个人学习使用,免费分享。每日更新,建议关注收藏!
目前[机试指南]本系列已经达到字数10w+,所以按原每个章节拆开发布,点击目录跳转。

本站友情链接:

  1. c/c++算法题指南
    严书代码
    c/c++大复习1
    c/c++大复习2
  2. python算法题指南
    牛客华为机试103精华
    python输入输出大全
    python语法
    PAT甲级真题刷题笔记 共179道
  3. python官方文档
    python官方文档
  4. 机试指南系列
    基础篇
    贪心篇
    递归分治搜索篇
    数据结构进阶篇(树/图/优先队列)
    数学问题篇
    动态规划篇
    STL篇

动态规划

基本思想同分治法,也是将问题分解成若干子问题,先求解子问题;
不同之处在于动态规划的问题分解后的子问题不是互相独立的;若用分治法,则子问题数目太多,存在大量重复计算;而动态规划会把子问题答案保存下来备用。
如fibonacci数列求解,其中有大量的重复计算,如fibonacci(7)要计算fibonacci(6)、fibonacci(5),fibonacci(6)也要计算fibonacci(5),为避免重复计算,设置数组dp[n]存放fibonacci(n),

动态规划专栏题库链接

dp3跳台阶扩展问题
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶(n为正整数)总共有多少种跳法。
f(n)=f(n-1)+f(n-2)+…+f(2)+f(1) (i)
f(n-1)=f(n-2)+…+f(1) (ii)
(i)&(ii)式得到f(n)=2f(n-1)
可以发现是等比数列,可得 n=1时f(n+1)=1,n>1即n>=2时f(n+1)=2f(n)

dp4:最小花费爬楼梯
给定一个整数数组 cost ,其中 cost[i] 是从楼梯第i 个台阶向上爬需要支付的费用,下标从0开始。一旦你支付此费用,即可选择向上爬一个或者两个台阶。你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。请你计算并返回达到楼梯顶部的最低花费。

#递归超时版
def go(cost,start,hadcost):
    end=length-1
    if(start+1==end):
        return hadcost+cost[start]
    elif(start==end):
        return hadcost+cost[end]
    else:
        return min(go(cost,start+1,hadcost+cost[start]),go(cost,start+2,hadcost+cost[start]))
length=int(input())
cost=list(map(int,input().split()))
x=min(go(cost,0,0),go(cost,1,0))
print(x)

#for循环 dp打表超省时
slong=int(input())
s=input().split()
s=[int(i) for i in s]
dp=[0,0]
for i in range(2,slong+1):
    dp.append(min(dp[i-1]+s[i-1],dp[i-2]+s[i-2]))
print(dp[slong])

dp26:跳跃游戏(本人博客文章中华为机试那篇的补充真题的变形)
给定一个非负整数数组 nums ,你最初位于数组的第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。

#数目过大时通过不了的代码 动态规划
import sys
for line in sys.stdin:
    a = line
    jump=[int(x) for x in input().split()]
    #print(jump)#下标i表示当前位置 jump[i]代表能跳的最大长度
    location=len(jump)-1
    dp=[]
    cnt=0
    
    i=0
    now=0
    while(now<len(jump) ):
        if(now==len(jump)-1):
            print('true')
            exit()
        for j in range(jump[i]+1):
            if((j+now) not in dp):
                dp.append(j+now)
        #print(dp,now)
        cnt+=1
        if(cnt>=len(dp)):break
        now=dp[cnt]
        i=now
    print('false')
'''
解决方法
采用贪心策略,从后向前遍历数组,看当前位置 + 值 是否 >= 给定的位置,最终判断是否为 0
'''

from typing import (
    List,
)

class Solution:
    """
    @param a: A list of integers
    @return: A boolean
    """
    def can_jump(self, a: List[int]) -> bool:
        # write your code here
        i=0
        max_i=0
        if(len(a)<=1):return 'true'
        while(i<len(a)-1):
            if(a[i]>0):
                max_i=max(max_i,i+a[i])
                i+=1
            elif(max_i>i):
                i+=1
            else:return 'false'
            print(i,max_i)
        if(i>=len(a)-1):return 'true'

www.lintcode.com/problem/114
1.数学题解法
机器人从左上角走到右下角,需要向下走m - 1步,向右走n - 1步,那么总步数也是一定的,为m + n - 2步。问题就转化成,从m + n - 2步中选出m - 1步向下,其余步数自然是向右,有多少种组合?即C(m-1,m+n-2)
2.动态规划打表 及优化
dp[i][j]=dp[i−1][j]+dp[i][j−1] 优化成dp[j]+=dp[j-1]
为什么能这样优化呢?因为dp[i][j]=dp[i−1][j]+dp[i][j−1] 等价于dp[i][j]+=dp[i][j−1] ,这样看 多出来的行坐标就不需要使用了

class Solution:
    """
    @param m: positive integer (1 <= m <= 100)
    @param n: positive integer (1 <= n <= 100)
    @return: An integer
    """
    def unique_paths(self, m: int, n: int) -> int:
        # write your code here
        dp=[0]*(m*n)
        #dp[i][j]表示走到i,j的路径数 dp[i][j]=dp[i-1][j]+dp[i][j-1]
        #优化成dp[j]+=dp[j-1] 当第i次遍历到dp[j]时,dp[j]表示到达(i, j)最多的路径数。
        j=0
        dp[j]=1
        for i in range(m):
            for j in range(n):
                dp[j]+=dp[j-1]
            print(dp)
        print(dp)
        return dp[n-1]

trick :双向dp

https://www.lintcode.com/problem/534/
由于是环形结构,所以要交叉的双向更新。

class Solution:
    # @param nums: A list of non-negative integers.
    # return: an integer
    def houseRobber2(self, nums):
        # write your code here
        n = len(nums)
        if n == 0:
            return 0
        if n == 1:
            return nums[0]

        dp = [0] * n
        
        dp[0], dp[1] = 0, nums[1] #舍去nums[0],则可以一直遍历到nums[-1]
        for i in range(2, n):
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])

        answer = dp[n - 1]

        dp[0], dp[1] = nums[0], max(nums[0], nums[1])#不舍去nums[0],则可以一直遍历到nums[-2]
        for i in range(2, n - 1):
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])

        return max(dp[n - 2], answer)

递归求解

例题12.1 N阶楼梯上楼问题(华中科技大学复试上机题) http://t.cn/Aij9Fr3V

import sys
for line in sys.stdin:
    a = int(line) 
    dp=[0 for i in range(a+1)]
    dp[0]=1
    for i in range(a+1):
        if(i<a):
            dp[i+1]+=dp[i]
        if(i+1<a):
            dp[i+2]+=dp[i]
    print(dp[-1])

习题12.1 吃糖果(北京大学复试上机题) http://t.cn/AiQsVyKz

import sys

for line in sys.stdin:
    c = int(line)
    dp=[0 for i in range(c+1)]
    dp[0]=1
    dp[1]=1 #dp[i]表示有i块的方案数
    if(c>=2):
        for i in range(2,c+1):
            dp[i]+=dp[i-1]
            dp[i]+=dp[i-2]
            #print(i,dp[i])
    print(dp[-1])

最大连续子序列乘积product

线性dp。由于数据中有正有负,所以我们利用两个dp数组来完成。用f[i]来保存计算到第i个时的最大值,用g[i]来保持计算到第i个时的最小值。
nums[i]的最大值f[i]=max(max(f[i-1]×nums[i], g[i-1]×nums[i]), nums[i])。g[i]=min(min(f[i-1]*×[i], g[i-1]×nums[i]), nums[i])。
这两串公式已经合并了两种情况,无非是一大一小或者两个相等:

  1. 继续连着乘 nums[i]
  2. 截断,从nums[I]起始

最后由于我们要求的是最大值,直接对f数组取最大值即可。

优化:计算到第i个时的最大值和计算到第i个时的最小值都用一个变量来表示和更新

class Solution:
    """
    @param nums: An array of integers
    @return: An integer
    """
    def maxProduct(self, nums):
        if not nums:
            return None
            
        global_max = prev_max = prev_min = nums[0]
        for num in nums[1:]:
            if num > 0:
                curt_max = max(num, prev_max * num) #max*num
                curt_min = min(num, prev_min * num)
            else:
                curt_max = max(num, prev_min * num) #min*num
                curt_min = min(num, prev_max * num) 
            print(prev_max,prev_min,curt_max,curt_min)
            global_max = max(global_max, curt_max)
            prev_max, prev_min = curt_max, curt_min
            
        return global_max

双数组

https://www.lintcode.com/problem/1310/
在不允许用除法以及时间复杂度必须o(n)情况下,如何解?
不允许用除法也避免了数组中有0的情况

解决方法:利用索引左侧所有数字的乘积和右侧所有数字的乘积(即前缀与后缀)相乘得到答案。对于给定索引 i,我们将使用它左边所有数字的乘积乘以右边所有数字的乘积。故建立左、右乘积两个数组
这两个数组的建立思想也与动态规划【dp双数组】类似。
举例:比如左乘积数组left,left[I]代表nums[I]左侧所有元素的乘积,left[i]=left[i-1]*nums[I-1];右侧同理。
right[I]代表nums[I]右侧所有元素的乘积

class Solution:
    """
    @param nums: an array of integers
    @return: the product of all the elements of nums except nums[i].
    """
    def product_except_self(self, nums: List[int]) -> List[int]:
        # write your code here
        left,right,answer=[1]*len(nums),[1]*len(nums),[1]*len(nums)
        for i in range(len(nums)):
            if(i>0):
                left[i]=left[i-1]*nums[i-1]

        for i in range(len(nums)-1,-1,-1):
            if(i<len(nums)-1):
                right[i]=right[i+1]*nums[i+1]
            #print(i,right)
        for i in range(len(nums)):
            answer[i]=left[i]*right[i]

        print(left,right,answer)
        return answer

最大连续子序列和

设置dp[i]表示以a[i]作为末尾的连续序列最大和。有两种情况

  1. 最大和的连续序列仅有一个元素a[i],则dp[i]=a[i]
  2. 多个元素,dp[i]=dp[i-1]+a[i]
    得到状态转移方程 dp[i]=max(a[i],dp[i-1]+a[i]) ,i从小到大枚举遍历

二维情况,求最大子矩阵和
当最大子矩阵所在行是i~j时,两种情况
i=j,则即求第i行元素最大连续子序列和
i!=j,把从第i行到第j行所有行元素加起来,得到只有一行的一位数组,这个一维数组的最大连续子序列和即最大子矩阵和
从小到大枚举遍历i和j
需要辅助矩阵将各个子矩阵包含的元素加起来,得到值

补充:前缀和,注意二维前缀和有坑!

前缀和&差分

一维前缀和
S[i] = a[1] + a[2] + … a[i] o(n)

  • a[l] + … + a[r] = S[r] - S[l - 1] 仅有o(1)复杂度

二维前缀和【画图】
S[i, j] = 第i行j列格子左上部分所有元素的和

  • 以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
    S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]

差分与前缀和是互逆运算

一维差分
已知前缀和a1,a2…ai,构造原本的数据b1…bi,

  • 可知ai=b1+…+bi,bi=a(i)-a(i-1)
  • 给区间[l, r]中的每个数加上c:B[l] += c, B[r + 1] -= c
  • 题目已知的a下标(1~n)的数据可看成每个单位数据分别由原本0插入得来,重用插入函数:ai的数值相当于原本全为0的数组,对范围内的a数组从l-r(l=i,r=i)添加c=a[i]得来

二维差分【画图】

  • 给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
    S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c
//一维前缀和
int n,m;
int a[N],s[N];
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	for(int i=1;i<=n;i++)s[i]=s[i-1]+a[i];
	while(m--){
		int l,r;
		cin>>l,r;
		printf("%d\n",s[r]-s[l-1]);
	}
	return 0;
}

//二维前缀和 画图做
int n,m,q;
int a[N][N],s[N][N];
int main(){
	cin>>n>>m>>q;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			scanf("%d",&a[i][j]);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			s[i][j]=s[i-1][j]+s[i][j-1]+s[i-1][j-1]+a[i][j];
	while(q--){
		int x1,x2,y1,y2;
		cin>>x1>>x2>>y1>>y2;
		//算部分和
		printf("%d\n",s[x2][y2]-s[x1-1][y2]-s[x2][y1-1]+s[x1-1][y1-1]);
	}
}

//一维差分
qs:输入整数序列,m个操作,
每个操作将序列[l,r]之间的每个数都加c,
输出操作后的序列。

int n,m;
int a[N],b[N];
void insert(int l,int r,int c){
	b[l]+=c;
	b[r+1]-=c;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	//原本两个矩阵均为0矩阵
	for(int i=1;i<=n;i++)insert(i,i,a[i]);//根据a[i]建立b[i]
	while(m--){
		int l,r,c;
		cin>>l>>r>>c;
		insert(l,r,c);//进行题目要求的操作
	}
	for(int i=1;i<=n;i++)b[i]+=b[i-1];///构造a数组
	for(int i=1;i<=n;i++)cout<<b[i];
	return 0;
}
//二维差分
给定矩阵,输入q个操作,
每个操作将(x1,y1),(x2,y2)位左上角坐标、右下角坐标规定的子矩阵中每个元素加c
输出操作后的矩阵

void insert(int x1,int x2,int y1,int y2){
	b[x1][y1]+=c;
	b[x2+1][y1]-=c;
	b[x1][y2+1]-=c;
	b[x2+1][y2+1]+=c;
}
int main(){
	cin>>n>>m>>q;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			scanf("%d",&a[i][j]);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			insert(i,j,i,j,a[i][j]);//将a每个元素看成一个矩阵,根据a构造b
	while(q--){
		int x1,x2,y1,y2;
		cin>>x1>>x2>>y1>>y2;
		insert(x1,y1,x2,y2,c);
	}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			b[i][j]+=b[i-1][j]+b[i][j-1]-b[i-1][j-1];//求和
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++)
			cout<<b[i][j]<<<" ";
		puts("");
	}
	return 0;
}

例题12.2 最大序列和(清华大学复试上机题) http://t.cn/AiYSlQMU

import sys

for line in sys.stdin:
    try:
        n = int(line)
        inputs=[ int(i) for i in input().split()]
        max_=0
        dp=[inputs[0]]
        for i in range(1,n):
            dp.append(max(inputs[i],dp[i-1]+inputs[i]))
        print(max(dp))
    except:
        pass
    try:
        p=input()
    except:
        pass

例题12.3 最大子矩阵(北京大学复试上机题) http://t.cn/AiYSemJz
重点注意,做了大半天也没做出来!!

#不知道为什么过不了
import sys

for line in sys.stdin:
    n = int(line) #n*n
    array=[[]for j in range(n)]
    max_value=-1200
    for i in range(n):
        array[i]=[int(x) for x in input().split()]
        temp=max(array[i])
        if(temp>max_value):max_value=temp
    #print(array)
    

    dp=[[0 for j in range(n)]for i in range(n)]
    #print(dp) #dp[i][j]表示以矩阵i~j行最大序列和

    assist=[[0 for i in range(n)] for j in range(n)] #矩阵[i][j]及左上角的值
    for i in range(n):
        for j in range(n):
            assist[i][j]+=array[i][j]
            if(j>0 and i==0):
                #print('assist',i,j,'+','assist',i,j-1,'=',assist[i][j],'+',)
                assist[i][j]+=assist[i][j-1]
                
            elif(i>0 and j==0):
                assist[i][j]+=assist[i-1][j]
            elif(i>0 and j>0):
                assist[i][j]+=(assist[i][j-1]+assist[i-1][j]-assist[i-1][j-1])

            if(assist[i][j]>max_value):max_value=assist[i][j]
    #print(assist)

    #print([assist[2][j]-assist[1][j] for j in range(n)])

    for i in range(0,n): ##dp[i][j]表示以矩阵i(包含)~j行(包含)最大序列和 
        for j in range(i,n):
            if(i==j and i):#求矩阵i行最大子序列之和
                dp[i][j]=max([assist[i][k]-assist[i-1][k] for k in range(n)])
            elif(i==j and i==0):
                dp[i][j]=max([assist[i][k] for k in range(n)])
            elif(i>=1):
                dp[i][j]=max([assist[j][k]-assist[i-1][k] for k in range(n)])
            else:
                dp[i][j]=max([assist[j][k] for k in range(n)])
            #print(i,j,dp[i][j])
            if(dp[i][j]>max_value):max_value=dp[i][j]
    #print(dp)
    print(max_value)
    
# 大神解法
n=int(input())
mat=[]
for _ in range(n):
    mat.append([int(i) for i in input().split()])
m,n=len(mat),len(mat[0])
pre=[[0]*(n+1) for _ in range(m+1)]
for i in range(1,m+1):
    for j in range(1,n+1):
        pre[i][j]=pre[i-1][j]+pre[i][j-1]-pre[i-1][j-1]+mat[i-1][j-1] #上来先打个二维前缀和的板子
        
def find(r1,c1,r2,c2):
    return pre[r2+1][c2+1]-pre[r2+1][c1]-pre[r1][c2+1]+pre[r1][c1]
    
res=float("-inf")
for r1 in range(n):#枚举上下行边界r1<r2,遍历纵坐标,接着就转化成一维的最大子数组和的问题
    for r2 in range(r1,n):
        total=0
        for c in range(n):
            total=max(total+find(r1,c,r2,c),find(r1,c,r2,c))
            res=max(res,total)
print(res)

习题12.2 最大连续子序列(浙江大学复试上机题) http://t.cn/AiYoUkjP

import sys

for line in sys.stdin:
    n = int(line) #>=1
    if(n==0):break
    nums=[int(x) for x in input().split()]
    flag=1
    if(sum(nums)<0):
        flag=0
        for item in nums:
            if(item>=0):
                flag=1
                break
    if(flag):
        first=[0] #index list
        last=0 #index 前闭后开
        dp=[nums[0]]
        max_sum=-10e6
        '''
        dp[j]代表到j的最大子序列 子序列最短可以就一个数
        dp[j]=max(dp[j-1],dp[j-1]+nums[j],nums[j])
        '''
        for i in range(1,n):
            temp=dp[-1]+nums[i]
            if(temp<nums[i]):
                #print('temp<nums[i]',temp,i,nums[i],max_sum)
                temp=nums[i]
                first.append(i)
                #print(first)
            if(temp>max_sum):
                #print('temp>max_sum',i,nums[i],temp,max_sum)
                max_sum=temp
                last=i              
            dp.append(temp)
        #print(dp)
        #print(first,last)
        for i in range(len(first)-1,-1,-1):
            if(first[i]>last):
                first.pop(i)
            else:
                break
        print(max(dp),nums[first[-1]],nums[last])
    else:
        print(0,nums[0],nums[-1])

最长递增子序列

dp[i]表示以a[i]作为末尾的最长递增子序列长度。
a[i]之前元素都比a[i]大,则dp[i]=1
若存在a[i]之前的元素a[j]比a[i]小,则dp[i]=dp[j]+1
得到状态转移方程 dp[i]=max(dp[i],dp[j]+1)

www.lintcode.com/problem/76

from typing import (
    List,
)

class Solution:
    def longest_increasing_subsequence(self, nums: List[int]) -> int:
        if not nums:
            return 0
        dp = []
        for i in range(len(nums)):
            dp.append(1)
            for j in range(i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[i], dp[j] + 1)
        return max(dp)

例题12.4 拦截导弹(北京大学复试上机题) http://t.cn/AiYCeV3m
本质:最长不增子序列(不一定连续)
dp[i]表示以a[i]作为末尾的最长不增子序列长度。
a[i]之前元素都比a[i]小,则dp[i]=1
若存在a[i]之前的元素a[j]比a[I]大,则dp[i]=dp[j]+1
得到状态转移方程 dp[i]=max(dp[i],dp[j]+1)

例如输入如下
9
98 51 21 93 38 65 41 46 9( 结果应该是5)
dp数组[1,2,3,2,3,3,4,4,5]
比如9对应的dp数组值为5因为其前面 98 51 21 93 38 65 41 46中dp最大的是为4的时候,也就是41和46所对应的dp值,也就是说9此刻对应的最长不增子序列为{98,93,65,41,9}或{98,93,65,46,9}
import sys

for line in sys.stdin:
    n = int(line)
    inputs=[int(i) for i in input().split()]

    dp=[1 for i in range(n)]
    for i in range(0,n):#back
        flag=0
        for j in range(i):#front
            if(inputs[i]<=inputs[j]):
                flag=1
                dp[i]=max(dp[j]+1,dp[i])
        if(flag==0):
            dp[i]=1
        #print(dp)
    print(max(dp))

例题12.5 最长上升子序列和(北京大学复试上机题) http://t.cn/AiYNAGD3
最大上升子序列和(不一定连续)
思路:统计以s[i]结尾的上升子序列的和sum[i]
分为2类:是否比前面所有元素都小
①是 前面序列对其没有贡献:sum[i]=s[i]
②否 前面序列对其有贡献:sum[i]=max(sum[j]+s[i])(需遍历s[i]前面所有元素,保留最大值)

#wa了很久 重点复盘!!!
n = int(input())
num = list(map(int, input().split(" ")))
dp = list(num)#记住,一定要加个list(),否则就是同一个内存地址
for i in range(n):
    for j in range(i):
        if num[j] < num[i]:
            dp[i] = max(dp[i], num[i] + dp[j])
print(max(dp))

习题12.3 合唱队形(北京大学复试上机题) http://t.cn/AiYNyHPe

from re import split
import sys

for line in sys.stdin:
    n = int(line)
    nums=[int(x) for x in input().split()] 
    dp1=[1 for i in range(n)]#最长递增子序列
    dp2=[1 for i in range(n)]#最长递减子序列
    for i in range(1,n):
        for j in range(i):
            if(nums[j]<nums[i]):
                dp1[i]=max(dp1[i],dp1[j]+1)

    for i in range(n-1,-1,-1):
        for j in range(n-1,i,-1):#从右往左
            if(nums[j]<nums[i]):
                dp2[i]=max(dp2[i],dp2[j]+1)    
    #print(dp1,dp2)
    dp=[]
    for k in range(n):
        left=dp1[k]
        right=dp2[k]
        dp.append(left+right-1)
    print(n-max(dp))

最长公共子序列Longest Common Subsequence

设置一个二维数组dp[][],令dp[i][j]表示以s1[i]作为末尾和以s2[j]作为末尾的最长公共子序列长度。最终dp[n][m]的值即为最终答案。
根据s1[i],s2[j]的关系可分为两种情况:
①s1[i]= s2[j],即 S1 中的第i个字符和 S2 中的第j个字符相同,此时必定存在一个最长公共子串以 s1[i],s2[j]结尾,其他部分等价于 s1 中前 i-1 个字符和s2中前j-1 个字符的最长公共子串,于是这个子串的长度比 dp[i-1][j-1]多1,
即 dp[i][j] = dp[i-1][j-1] + 1。
② S1[i] != S2[j],此时最长公共子串长度为 s1 中前 i-1个字符和 S2 中前j 个字符的最长公共子串长度与 S1 中前i个字符和 s2 中前 j-1 个字符的最长公共子串长度的较大者,即在两种情况下得到的最长公共子串都不会因为其中一个字符串
又增加了一个字符长度而发生改变,也就是dp[i][j]=max{dp[i-1][j],dp[i][j-1]}
从这两种情况可以得到状态转移方程:
s1[i]= s2[j]: dp[i][j] = dp[i-1][j-1] + 1
S1[i] != S2[j]: dp[i][j]=max{dp[i-1][j],dp[i][j-1]}

而对于边界情况,如果两个字符串中的其中一个为空串,那么公共字符串的长度为0 ,得到:
dp[i][0] = 0 (0 <= i <= n)
dp[0][j] = 0 (0 <= j <= m)
由这样的状态转移,只需依次遍历 i 和 j便能求得各个 dp[i][j]的值,其时间复杂度O(mn)

www.lintcode.com/problem/77

class Solution:
    """
    @param a: A string
    @param b: A string
    @return: The length of longest common subsequence of A and B
    """
    def longest_common_subsequence(self, a: str, b: str) -> int:
        # write your code here
        #dp[i][j]表示以s1[i]作为末尾和以s2[j]作为末尾的最长公共子序列长度
        if(len(a)==0 or len(b)==0):return 0
        dp=[]
        for i in range(0,len(a)):
            dp.append([0]*len(b))
            for j in range(0,len(b)):
                if(a[i]==b[j]):
                    if(i and j):
                        dp[i][j]=dp[i-1][j-1]+1
                    else:dp[i][j]=1
                    #print(i,j,dp)
                else:
                    if(i and j):
                        dp[i][j]=max(dp[i-1][j],dp[i][j-1])
                    else:dp[i][j]=0
        #print(dp)
        return max(dp[-1])

例题12.6 Common Subsequence http://acm.hdu.edu.cn/showproblem.php?pid=1159

习题12.4 Coincidence(上海交通大学复试上机题) http://t.cn/AiY03RO5

import sys

for line in sys.stdin:
    a = line
    b=input()
    a=a[:-1]
    dp=[[0 for i in range(len(b))]for j in range(len(a))] #dp[i][j]指a[i]和b[j]为末尾的最长公共子序列
    #print(dp)
    for i in range(len(a)):
        for j in range(len(b)):
            if(a[i]==b[j] and i>0 and j>0):
                dp[i][j]=dp[i-1][j-1]+1
            elif(a[i]==b[j] and (i==0 or j==0)):
                dp[i][j]=1
            else:
                dp[i][j]=max(dp[i-1][j],dp[i][j-1])
    print(max(dp[-1]))

背包问题

0-1背包
有n件物品,每件物品重量为w[i],价值为v[i],有容量为m的背包,如何选择物品使装入背包物品的价值最大?
动态规划使得这个问题从复杂度o(2^n)->o(mn)

设置二维数组dp[][],令dp[i][j]表示前i个物品装进容量为j的背包能获得的最大价值,则最终解是dp[n][m]
考虑第i件物品时,情况分是否放入第i件物品,即两种情况

  1. 容量为j的背包,如果不放入第i件物品,则问题转换成将前i-1个物品放入容量为j的背包问题,dp[i][j]=dp[i-1][j]
  2. 容量为j的背包,如果放入第i件物品,则背包容量变成j-wi,得到物品价值v[i],问题转换成将前i-1个物品放入容量为j-w[i]背包的问题,dp[i][j]=dp[i-1][j-w[i]]+v[i]
  3. 总结为状态转移方程 dp[i][j]=max(dp[i-1][j] , dp[i-1][j-w[i]]+v[i])

边界情况,如果装入0件物品,价值必定为0;如果背包容量为0,价值必定为0.
dp[i][0]=dp[0][j]=0 (0<=i<=n,0<=j<=m)

优化: dp[i][j]转移仅与dp[i-1][j] , dp[i-1][j-w[i]]+v[i]有关,即二维数组中本行的上一行有关,可以优化成一维数组
dp[j]=max(dp[j],dp[j-w[i]]+v[i])

为了保证状态正确转移,必须保证在每次更新中确定状态dp[j]时,dp[j-w[i]]尚未被本次更新修改。 因为dp[i][j]转移仅与dp[i-1][j] , dp[i-1][j-w[i]]+v[i]有关,都是和上一行有关的,而容量是越放越小的,这就需要在每次更新中,倒序地遍历所有j值,因为只有这样才能保证在确定dp[j]的值时,dp[j-w[i]]的值尚未被修改,从而完成正确的状态转移。

0-1背包是最基本的背包问题,其他背包问题都是由这个演变。
0-1背包的特点:每件物品最多只能选择1件,即在背包中该物品的数量只有0或1两种情况

例题12.7 点菜问题(北京大学复试上机题) http://t.cn/AiYOrkXr

import sys
for line in sys.stdin:
    c,n = line.split()
    c=int(c)
    n=int(n)
    dishes=[]
    dp=[0 for i in range(c+1)] #dp[j]表示容量为j时的最大分数,坑:这里容量最大为c,所以range最大是c+1
    for i in range(n):
        price,mark=input().split()
        dishes.append([int(price),int(mark)])
    dishes=sorted(dishes,key=lambda x:x[0])
    #print(dishes)
    for k in dishes:
        for j in range(len(dp)-1,-1,-1):
            if(k[0]>j):break
            dp[j]=max(dp[j],dp[j-k[0]]+k[1])
    print(dp[-1])

习题12.6 最小邮票数(清华大学复试上机题) http://t.cn/AiYlwchD

''' 难!卡了很久,看下大神思路。
dp[i][j]表示只用前i种邮票能凑成总值M的最少邮票数,如果凑不成为INF
dp[0][j] = INF
dp[i][0] = 0
dp[i][j] = min(dp[i - 1][j], dp[i - 1][j - p[i]] + 1)
'''
from re import T
import sys

for line in sys.stdin:
    m = int(line)
    n= int(input())
    stamps=[int(x) for x in input().split()] 
    inf=100
    dp=[inf for i in range(m+1)]#dp[i]表示组成i的最小张数
    dp[0]=0
    for i in range(n):
        for j in range(m,stamps[i]-1,-1):
            dp[j]=min(dp[j],dp[j-stamps[i]]+1)
    if(dp[m]==inf):print(0)
    else:print(dp[m])
 

完全背包
如果将0-1背包扩展,每件物品可以选择多件(无限个),得到完全背包问题:n种物品,每种物品重量w[i],价值v[i],每种物品数量为无限个,背包容量m,如何选使装入物品的价值最大?

设置二维数组dp[][],令dp[i][j]表示前i个物品装进容量为j的背包能获得的最大价值,则最终解是dp[n][m]
考虑第i件物品时,情况分是否放入第i件物品,即两种情况

  1. 容量为j的背包,如果不放入第i件物品,则问题转换成将前i-1个物品放入容量为j的背包问题,dp[i][j]=dp[i-1][j]
  2. 容量为j的背包,如果放入第i件物品,则背包容量变成j-wi,得到物品价值v[i],但由于第i件物品依然可取,问题转换成dp[i][j]=dp[i][j-w[i]]+v[i],而不是之前的dp[i][j]=dp[i-1][j-w[i]]+v[i]
  3. 总结为状态转移方程 dp[i][j]=max(dp[i-1][j] , dp[i][j-w[i]]+v[i])

边界情况,如果装入0件物品,价值必定为0;如果背包容量为0,价值必定为0.
dp[i][0]=dp[0][j]=0 (0<=i<=n,0<=j<=m)

优化: dp[i][j]转移与i-1 或者i 无关,可以优化成一维数组 dp[j]=max(dp[j],dp[j-w[i]]+v[i])

为了保证状态正确转移,必须保证在每次更新中确定状态dp[j]时,dp[j-w[i]]已完成本次更新修改, 因为dp[i][j]=max(dp[i-1][j] , dp[i][j-w[i]]+v[i]),和上一行和左一列有关
因为物品可以取n次,这就需要在每次更新中,正序地遍历所有j值,因为只有这样才能保证在确定dp[j]的值时,dp[j-w[i]]的值已经被修改,从而完成正确的状态转移。

例题12.8 Piggy-Bank http://acm.hdu.edu.cn/showproblem.php?pid=1114

类似完全背包 www.lintcode.com/problem/564/
记录加起来等于某个值的可能数,最后取等于target的输出

from typing import (
    List,
)

class Solution:
    """
    @param nums: an integer array and all positive numbers, no duplicates
    @param target: An integer
    @return: An integer
    """
    def back_pack_v_i(self, nums: List[int], target: int) -> int:
        dp = [1] + [0] * target
        for i in range(1, target + 1):
            for num in nums:
                if num <= i:#num是题目包含的给出的,只需要知道i-num有几种可能
                    dp[i] += dp[i - num]
                print(num,i,dp)
        return dp[target]

多重背包
0-1背包和完全背包之间的问题,即每种物品最多取k件。n种物品,每种物品重量w[i],价值v[i],每种物品数量为k[i],背包容量m,如何选使装入物品的价值最大?
可以将多重背包转换成01背包问题,即每件物品均视为k种重量价值都相同的物品,对所有物品求01背包。
由于降低数量会降低复杂度,则将原数量为k的物品拆分成若干组,将每组物品视为一件物品,价值是该组物品所有的价值之和。每组物品包含的物品个数为20,21,…2^(c-1), k-2c+1,(c是使得k-2c+1>=0的最大整数)。
这种分法使得复杂度相较于单个拆开看成物品,复杂度从O( ki的和m)-> O(logki的和m)

例题12.9 珍惜现在,感恩生活 http://acm.hdu.edu.cn/showproblem.php?pid=2191

其他问题

例题12.10 The Triangle http://poj.org/problem?id=1163

例题12.11 Monkey Banana Problem http://lightoj.com/volume_showproblem.php?problem=1004

习题12.7 放苹果(北京大学复试上机题) http://t.cn/AiQsyOnq
习题12.8 整数拆分(清华大学复试上机题) http://t.cn/AiQsUM0Q

www.lintcode.com/problem/107
dp[i] = s[0:i]能否分割成dict里的单词。i往右移动,j从0向i找,如果dp[j] is True and s[j:i]存在于dict,说明dp[i] is True。一旦找到就可以break。

#可惜了,这个最后一个测试点超时
class Solution:
    """
    @param s: A string
    @param word_set: A dictionary of words dict
    @return: A boolean
    """
    def wordBreak(self, s, dict):
        n = len(s)
        dp = [False for _ in range(n + 1)]
        dp[0] = True 
        for i in range(1, n + 1):
            for j in range(i):
                if not dp[j]:
                    continue
                if s[j:i] in dict:
                    dp[i] = True
                    break 
        return dp[-1]

#优化版
class Solution:
    """
    @param: s: A string
    @param: dict: A dictionary of words dict
    @return: A boolean
    """
    def wordBreak(self, s, wordSet):
        if not s:
            return True
            
        n = len(s)
        dp = [False] * (n + 1)
        dp[0] = True
        
        max_length = max([
            len(word)
            for word in wordSet
        ]) if wordSet else 0
        
        for i in range(1, n + 1):
            for l in range(1, max_length + 1):#优化之处:j这里取值范围缩短了
                if i < l:
                    break
                if not dp[i - l]:
                    continue
                word = s[i - l:i]
                if word in wordSet:
                    dp[i] = True
                    break
        
        return dp[n]

https://www.lintcode.com/problem/512/
beat all 100%的解法

class Solution:
    """
    @param s: a string,  encoded message
    @return: an integer, the number of ways decoding
    """
    def num_decodings(self, s: str) -> int:
        # write your code here
        try:
            if(len(s)==0 or s[0]=='0' or s.index('00')>=0):return 0
        except:
            pass
        dp=[1]+[0]*(len(s)-1)
        #print(dp)
        
        for i in range(1,len(s)):
            if(s[i]=='0'):
                temp=int(s[i-1:i+1])
                if(temp<10 or temp>=30):
                    return 0 #当出现0 又和前面一个字符拼不起来一个解码时 0种可能
                if(i>=2):
                    dp[i]=dp[i-2] 
                    #当出现0 且和前面一个字符可以拼成解码时 这两个就绑定起来了 
                    #无论怎么解码,这两个都是组合解码,所以可能性+1(组合),-1(单个)等于不变
                else:dp[i]=1
            else: 
                dp[i]=dp[i-1] #不会增加的原因是 多了这一个字符,依然只是将其单个解码,可能性不变
                temp=int(s[i-1:i+1])
                if(temp>=10 and temp<=26):#如果可以拼成组合解码 
                    if(i>=2):dp[i]+=dp[i-2]#dp[i]=dp[i-1]+dp[i-2] 因为是单个解码+组合解码
                    else:dp[i]+=1#当i==1时,增加一种可能
        print(dp)
        return dp[-1]
        
  • 7
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值