算法课8-Dynamic Programming⭐️

动态规划是常考算法,将指数级的问题降到多项式级别。

动态规划和分治法都是将问题划分成子问题进行求解,它们的区别主要是:

  • 分治法的子问题无重叠
  • 动态规划的子问题有重叠,并且重叠的个数是指数级别的

动态规划和贪心法的相同之处是原问题包含子问题的最优解,而它们的区别在于:

  • 贪心法只看局部最优,最终达到全局最优
  • 对于动态规划局部最优不一定是全局最优

1. Weighted interval scheduling
带权重的最大兼容子集,不能用贪心法进行求解,需要考虑动态规划的解法。首先将活动按照结束时间排序。两种情况,一种是选当前的j,那么最优解等于从后往前找第一个与j不冲突的活动(使用二分查找加快速度)的opt加上j的权重,一种是不选j,那么最优解等于j的前一个活动的最优解。如下式:
o p t ( j ) = m a x { v j + o p t ( p ( j ) ) , o p t ( j − 1 ) } opt(j)=max\{v_j+opt(p(j)),opt(j-1)\} opt(j)=max{vj+opt(p(j)),opt(j1)}
ps.若按照开始时间排序,要反过来找,那么opt(i)可以定义为,i~n个活动,p(i)是从前往后找第一个不冲突的活动。opt(n+1)=0,return opt(1)。
o p t ( i ) = m a x { v i + o p t ( p ( i ) ) , o p t ( i + 1 ) } opt(i)=max\{v_i+opt(p(i)),opt(i+1)\} opt(i)=max{vi+opt(p(i)),opt(i+1)}

2. Segmented least squares
线段拟合点集问题,希望线段尽量少,error尽量小。
error定义为E+cL ,E是平方误差之和,L是线段条数。
opt(j) 是 p 1 . . . p j p_1...p_j p1...pj的最小error,e(i,j)是 p i , p i + 1 . . . p j p_i,p_{i+1}...p_j pi,pi+1...pj的最小平方误差
o p t ( j ) = min ⁡ 1 ≤ i ≤ j { e i j + c + o p t ( i − 1 ) } opt(j)=\min \limits_{1\leq i\leq j}\{e_{ij}+c+opt(i-1)\} opt(j)=1ijmin{eij+c+opt(i1)}
这个算法的瓶颈在于计算 e i j e_{ij} eij,对j要扫一遍,j固定了,i要扫一遍,i固定了,在i j之间要扫一遍, 使得算法的效率是 O ( n 3 ) O(n^3) O(n3).

3. 买卖股票系列

  • 只能买卖一次
    Best Time to Buy and Sell a Stock
    f(j):第j次卖出的最优解, f(1)=0, return max ⁡ 1 ≤ j ≤ n ( f ( j ) ) \max\limits_{1\leq j\leq n}(f(j)) 1jnmax(f(j)) ,两种情况,第一种是当天卖出,第二种是前j-1天买入,第j天卖出,第二种情况和第j-1天卖出的区别就是第j天的价格减去第j-1天的价格。
    f ( j ) = m a x ( 0 , f ( j − 1 ) + p j − p j − 1 ) f(j)=max(0,f(j-1)+p_{j}-p_{j-1}) f(j)=max(0,f(j1)+pjpj1)
    一个直观的想法是说维护到目前为止价格的最低点,用上一行减去下一行即可得到最大利润。
5 2 7 13 1 9
5 2 2 2 1 1

这道题还可以用分治法做,详见上一篇博文。

  • 买卖两次
    Best Time to Buy and Sell Stock III
    设置两个dp数组, f 1 ( i ) f_1(i) f1(i)是第i天第一次卖出最大收益, f 2 ( i ) f_2(i) f2(i)是第i天第二次卖出最大收益。
    思路1:
    g 1 ( j ) = max ⁡ 1 ≤ i ≤ j f 1 ( i ) g_1(j)=\max\limits_{1\leq i \leq j}{f_1(i)} g1(j)=1ijmaxf1(i) 即为前j天的最大收益
    f 2 ( j ) = max ⁡ 1 ≤ i ≤ j { p j − p i + g 1 ( i − 1 ) } f_2(j)=\max\limits_{1\leq i \leq j}\{p_j-p_i+g_1(i-1)\} f2(j)=1ijmax{pjpi+g1(i1)}
    时间 θ ( n 2 ) \theta (n^2) θ(n2) 空间 θ ( n ) \theta (n) θ(n)
    思路2:
    f 2 ( j ) = m a x ( g 1 ( j − 1 ) , f 2 ( j − 1 ) + p j − p i ) f_2(j)=max(g1(j-1),f_2(j-1)+p_j-p_i) f2(j)=max(g1(j1),f2(j1)+pjpi)
    return max ⁡ 1 ≤ j ≤ n ( f 2 ( j ) ) \max\limits_{1\leq j \leq n}(f_2(j)) 1jnmax(f2(j))
    时间 θ ( n ) \theta(n) θ(n) 空间 θ ( n ) \theta(n) θ(n)

代码:

#include 
using namespace std;

int sale[100005];
int t;
int dp1[100005];
int dp1_max[100005];
int dp2[100005];
int main(){
    scanf("%d",&t);
    while(t--){
        int n;
        scanf("%d",&n);
        for(int i=0;i<n;i++){
            scanf("%d",&sale[i]); //这里使用cin会超时
            dp1[i]=0;
            dp2[i]=0;
        }
        int res=0;
        for(int i=1;i<n;i++){
            dp1[i]=max(dp1[i-1]+sale[i]-sale[i-1],0);
            dp1_max[i]=max(dp1_max[i-1],dp1[i]); //最大值存储别写错
            dp2[i]=max(dp1_max[i-1],dp2[i-1]+sale[i]-sale[i-1]);
            res=max(res,dp2[i]);
        }
        cout<<res<<endl;
    }
    return 0;
}
  • 买卖k次
    Best time to Buy and Sell Stock IV
    以下解法借鉴自disscuss

    • dp[i, j] represents the max profit up until prices[j] using at most i transactions.
    • dp[i, j] = max(dp[i, j-1], prices[j] - prices[jj] + dp[i-1, jj]) { jj in range of [0, j-1] }
      = max(dp[i, j-1], prices[j] + max(dp[i-1, jj] - prices[jj]))
    • dp[0, j] = 0; 0 transactions makes 0 profit
    • dp[i, 0] = 0; if there is only one price data point you can’t make any transaction.

public int maxProfit(int k, int[] prices) {
	int n = prices.length;
	if (n <= 1)
		return 0;
	
	//if k >= n/2, then you can make maximum number of transactions.
	if (k >=  n/2) {
		int maxPro = 0;
		for (int i = 1; i < n; i++) {
			if (prices[i] > prices[i-1])
				maxPro += prices[i] - prices[i-1];
		}
		return maxPro;
	}
	
    int[][] dp = new int[k+1][n];
    for (int i = 1; i <= k; i++) {
    	int localMax = dp[i-1][0] - prices[0];
    	for (int j = 1; j < n; j++) {
    		dp[i][j] = Math.max(dp[i][j-1],  prices[j] + localMax);
    		localMax = Math.max(localMax, dp[i-1][j] - prices[j]);
    	}
    }
    return dp[k][n-1];
}
  • 买卖随意多次,有冷静期
    Best time to buy and sell stock with cooldown
public int maxProfit(int[] prices) {
		//当前是买入状态(k<=i天买入,没卖出)
        int[]hold=new int[prices.length+1];
        int[]sold=new int[prices.length+1];//卖出状态
        int[]rest=new int[prices.length+1];//冷静状态
        hold[0]=Integer.MIN_VALUE;
        sold[0]=0;
        rest[0]=0;
        for(int i=1;i<prices.length+1;i++){
       		//买前需要冷静
            hold[i]=Math.max(hold[i-1],rest[i-1]-prices[i-1]);
            //卖前需要买
            sold[i]=hold[i-1]+prices[i-1];
            //冷静期之前要么是休息要么是卖掉了
            rest[i]=Math.max(rest[i-1],sold[i-1]);
        }
        return Math.max(rest[prices.length],sold[prices.length]);
    }

以下是 θ ( 1 ) \theta(1) θ(1)空间解法和思路重现
Share my thinking process

  • 买卖随意多次,每次交易都有个费用
    Best time to buy and sell stock with transaction fee
    2 solutions 2 states DP solutions
    两种状态买卖,可以将费用算在买或者卖状态上
public int maxProfit(int[] prices, int fee) {
        if (prices.length <= 1) return 0;
        int days = prices.length, buy[] = new int[days];
        int sell[] = new int[days];
        buy[0]=-prices[0]-fee;
        for (int i = 1; i<days; i++) {
        // keep the same as day i-1, or buy from sell status at day i-1
            buy[i] = Math.max(buy[i - 1], sell[i - 1] - prices[i] - fee); 
        // keep the same as day i-1, or sell from buy status at day i-1
            sell[i] = Math.max(sell[i - 1], buy[i - 1] + prices[i]); 
        }
        return sell[days - 1];
    }

4. 最长递增子序列
LeetCode Longest increasing subsequence
一个错误的思路
dp[i]=max{dp[p(i)]+1,dp[i-1]} 选i或者不选i,如果选,从后往前找到第一个小于i的值的dp值加1,这个思路不对的原因在于,没办法保证第一个小于i的值的状态是选了这个值的状态,也就是说,可能没有选这个p(i),它的当前最大值不能保证合法。

  • 思路1
    dp[i]是以i结尾的最长递增子序列,那么状态转移应该是,从i往前找,比i小的位置j的dp值加1中的最大值,注意,后面的数不一定比前面的大。最后返回的是dp值中最大的那一个。
public int lengthOfLIS(int[] nums) {
        if(nums.length==0) return 0;
        int[]dp=new int[nums.length];
        Arrays.fill(dp,1);
        int result=1;
        for(int i=1;i<nums.length;i++){
            for(int j=i-1;j>=0;j--){
                if(nums[j]<nums[i]){
                   dp[i]=Math.max(dp[i],dp[j]+1);    
                   
                }
            }
            result=Math.max(result,dp[i]);          
        }
        return result;
    }

时间复杂度 θ ( n ) \theta(n) θ(n)

  • 思路2
    可以有 n l o g n nlogn nlogn的思路,很自然地想到二分查找的基本思想。这个思路是说,维护一个数组tail,tail[i]是长度为i-1的递增子序列的最小末尾。遍历数组,每次的元素x有两种情况,一种是x大于所有的tail,那么size++, 一种是x在tail[i-1]和tail[i]之间,更新tail[i]。寻找x范围的过程用二分。
public int lengthOfLIS(int[] nums) {
    int[] tails = new int[nums.length];
    int size = 0;
    for (int x : nums) {
        int i = 0, j = size;
        //求最接近且小于当前值的二分
        while (i != j) {
            int m = (i + j) / 2;
            if (tails[m] < x)
                i = m + 1;
            else
                j = m;
        }
        tails[i] = x;
        if (i == size) ++size;
    }
    return size;
}

作业题
合唱队形
描述
N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学不交换位置就能排成合唱队形。
合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1, 2, …, K,他们的身高分别为T1, T2, …, TK,则他们的身高满足T1 < T2 < … < Ti , Ti > Ti+1 > … > TK (1 <= i <= K)。
你的任务是,已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
输入
输入的第一行是一个整数N(2 <= N <= 100),表示同学的总数。第一行有n个整数,用空格分隔,第i个整数Ti(130 <= Ti <= 230)是第i位同学的身高(厘米)。
输出
输出包括一行,这一行只包含一个整数,就是最少需要几位同学出列。
样例输入

8
186 186 150 200 160 130 197 220

样例输出

4

这道题是LIS的变种,其实就是维护两个最长递增子序列,后面那个递减的倒着来就是递增了。

#include 
using namespace std;

int height[105];
int dp1[105];
int dp2[105];
int n;
int main(){
    while(scanf("%d",&n)!=EOF) {
        for (int i = 0; i < n; i++) {
            cin >> height[i];
            dp1[i] = 1;
            dp2[i] = 1;
        }


        for (int i = 1; i < n; i++) {
            for (int j = i - 1; j >= 0; j--) {
                if (height[j] < height[i]) {
                    dp1[i] = max(dp1[i], dp1[j] + 1);
                }

            }
        }
        for (int i = n - 2; i >= 0; i--) {
            for (int j = n - 1; j > i; j--) { //注意循环的边界
                if (height[j] < height[i]) {
                    dp2[i] = max(dp2[i], dp2[j] + 1);
                }
            }
        }
        int result = 0;
        for (int i = 0; i < n; i++) {
            result = max(result, dp1[i] + dp2[i] - 1);
        }
        cout << n - result << endl;
    }

    return 0;
}

5. 子序列和最大系列

  • 经典形式:连续子数组序列和最大
    f ( j ) f(j) f(j)-以第j个元素结尾的最大和
    f ( j ) = a j + m a x ( 0 , f ( j − 1 ) ) f(j)=a_j+max(0,f(j-1)) f(j)=aj+max(0,f(j1))
    return m a x { f ( j ) } max\{f(j)\} max{f(j)}

  • 连续子数组和绝对值最大
    求一个最大和,再求一个最小和,再取绝对值求最大

  • 子序列乘积最大
    因为存在负负得正的情况,需要维护一个最大乘积和一个最小乘积(可能是负值再乘一个负值就可能变成最大)
    f m a x ( j ) = m a x { a j a j ∗ f m a x ( j − 1 ) a j ∗ f m i n ( j − 1 ) f_{max}(j)=max\left\{ \begin{aligned} a_j \\ a_j * f_{max}(j-1) \\ a_j*f_{min}(j-1) \end{aligned} \right. fmax(j)=maxajajfmax(j1)ajfmin(j1)
    f m i n ( j ) = m i n { a j a j ∗ f m a x ( j − 1 ) a j ∗ f m i n ( j − 1 ) f_{min}(j)=min\left\{ \begin{aligned} a_j \\ a_j * f_{max}(j-1) \\ a_j*f_{min}(j-1) \end{aligned} \right. fmin(j)=minajajfmax(j1)ajfmin(j1)

  • 子序列和最大,但长度受限(不超过m)
    naive 解法:j往前m个求最大 θ ( n m ) \theta(nm) θ(nm)
    总体思路:
    维护一个累加和数组, S j S_j Sj表示 a 1 + a 2 + . . . + a j a_1+a_2+...+a_j a1+a2+...+aj,我们需要得到的是最大的 S j − S i S_j-S_i SjSi,在j确定的情况下,只需要找到前m个中最小的 S i S_i Si
    思路一:
    用一个最小堆维护m个元素,维护长度为m的滑动窗口,每次删除窗口外最前的一个元素。
    删除操作的实现:
    ①使用结构体struct {i,s[i]} ,在pop的时候发现i与j的差距超过m, continue θ ( n l o g n ) \theta(nlogn) θ(nlogn)
    ②使用支持删除任意元素的堆(STL库不支持) θ ( n l o g m ) \theta(nlogm) θ(nlogm)

    思路2:
    使用一个双向队列,size是m,最小值是队列的头元素,维护对应的index保证队列里面的数字和当前的index差m以内。队列中直接存index就行。
    这个队列每次进来一个元素,pop掉所有比它大的元素。(类比:提拔干部时同等能力下选择年轻人,在这里元素越小能力越强,后进来的元素又年轻又强,则轮不到前面的老人,把它们删除。)
    解法:https://blog.csdn.net/SSimpLe_Y/article/details/71792893

  • 子矩阵和最大
    描述
    已知矩阵的大小定义为矩阵中所有元素的和。给定一个矩阵,你的任务是找到最大的非空(大小至少是1 * 1)子矩阵。

    比如,如下4 * 4的矩阵

    0 -2 -7 0
    9 2 -6 2
    -4 1 -4 1
    -1 8 0 -2

    的最大子矩阵是

    9 2
    -4 1
    -1 8

    这个子矩阵的大小是15。
    输入
    输入是一个N * N的矩阵。输入的第一行给出N (0 < N <= 100)。再后面的若干行中,依次(首先从左到右给出第一行的N个整数,再从左到右给出第二行的N个整数……)给出矩阵中的N2个整数,整数之间由空白字符分隔(空格或者空行)。已知矩阵中整数的范围都在[-127, 127]。
    输出
    输出最大子矩阵的大小。
    样例输入

    4
    0 -2 -7 0 9 2 -6 2
    -4 1 -4 1 -1
    8 0 -2

    样例输出

    15

    将二维问题转化为一维问题,假设行的范围确定,为i~j,那么横着将i到j的元素值相加即对应列元素的和,得到一组序列,再用连续子数组和最大来解。这组序列的长度也需要遍历,所以是 θ ( n 3 ) \theta(n^3) θ(n3)的时间复杂度。

#include 
#include 
#define INF 12700
using namespace std;

int nums[105][105];
int dp[105];

int main(){
    int n;
    while(scanf("%d",&n)!=EOF){
        for(int i=0;i<n;i++){
            for(int j=0;j<n;j++){
                cin>>nums[i][j];
            }
        }
        int result=-INF;
        for(int i=0;i<n;i++){
            memset(dp,0, sizeof(dp));
            for (int j = i; j < n; ++j) {
                int sum=0;
                for(int k=0;k<n;k++){
                    dp[k]+=nums[j][k];
                    sum+=dp[k];
                    if(sum<0) sum=dp[k];
                    result=max(result,sum);
                }
            }
        }
        cout<<result<<endl;
    }
    return 0;
}

作业题 棋盘分割

描述
将一个8*8的棋盘进行如下分割:将原棋盘割下一块矩形棋盘并使剩下部分也是矩形,再将剩下的部分继续如此分割,这样割了(n-1)次后,连同最后剩下的矩形棋盘共有n块矩形棋盘。(每次切割都只能沿着棋盘格子的边进行)

原棋盘上每一格有一个分值,一块矩形棋盘的总分为其所含各格分值之和。现在需要把棋盘按上述规则分割成n块矩形棋盘,并使各矩形棋盘总分的均方差最小。
均方差,其中平均值,xi为第i块矩形棋盘的总分。
请编程对给出的棋盘及n,求出O’的最小值。
输入
第1行为一个整数n(1 < n < 15)。
第2行至第9行每行为8个小于100的非负整数,表示棋盘上相应格子的分值。每行相邻两数之间用一个空格分隔。
输出
仅一个数,为O’(四舍五入精确到小数点后三位)。
样例输入

3
1 1 1 1 1 1 1 3
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 0
1 1 1 1 1 1 0 3

样例输出

1.633

把方差展开,由于棋盘总和的平均值一定, S 2 = 1 n ∑ ( X i − X ˉ ) 2 = 1 n ∑ X i 2 − 2 ∑ X i X ˉ + ∑ X ˉ 2 = 1 n ∑ X i 2 − X ˉ S^2=\frac{1}{n}\sum(X_i-\bar{X})^2=\frac{1}{n}\sum X_i^2-2\sum X_i\bar{X}+\sum\bar{X}^2=\frac{1}{n}\sum X_i^2-\bar{X} S2=n1(XiXˉ)2=n1Xi22XiXˉ+Xˉ2=n1Xi2Xˉ
则只需要求分割后棋盘的平方和最小就行了。维护一个累加和数组,sum[i][j]是以(0,0)为左上角,(i,j)为右下角的累加和。枚举竖着切和横着切的情况,用记忆数组存结果来剪枝。

#include 
#include 
#include 
#include 

using namespace std;

int nums[8][8];
int n;
int sum[8][8];
int memo[15][8][8][8][8];

int get_sum(int x1,int y1,int x2,int y2){
    if(x1>0 && y1>0) {
        return sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1];
    }
    if(x1>0){
        return  sum[x2][y2]-sum[x1-1][y2];
    }
    if(y1>0){
        return  sum[x2][y2]-sum[x2][y1-1];
    }
    return  sum[x2][y2];
}


int cut(int n,int x1,int y1,int x2,int y2){
    if(memo[n][x1][y1][x2][y2]!=-1) {
        return memo[n][x1][y1][x2][y2];
    }
    int MIN=10000000;
    if(n==1){
        int a=get_sum(x1,y1,x2,y2);
        memo[n][x1][y1][x2][y2]=a*a;
        return a*a;
    }
    for(int i=x1;i<x2;i++){
        int a=get_sum(i+1,y1,x2,y2);
        int b=get_sum(x1,y1,i,y2);
        int res=min(cut(n-1,x1,y1,i,y2)+a*a,cut(n-1,i+1,y1,x2,y2)+b*b);
        MIN=min(MIN,res);
    }
    for(int i=y1;i<y2;i++){
        int a=get_sum(x1,i+1,x2,y2);
        int b=get_sum(x1,y1,x2,i);
        int res=min(cut(n-1,x1,y1,x2,i)+a*a,cut(n-1,x1,i+1,x2,y2)+b*b);
        MIN=min(MIN,res);
    }
    memo[n][x1][y1][x2][y2]=MIN;

    return MIN;

}
int main(){
    while(scanf("%d",&n)!=EOF){
        memset(memo,-1, sizeof(memo));
        memset(sum,0, sizeof(sum));
        for(int i=0;i<8;i++){
            int rowsum=0;
            for(int j=0;j<8;j++){
                cin>>nums[i][j];
                rowsum+=nums[i][j];
                if(i>0) {
                    sum[i][j] += sum[i - 1][j] + rowsum;
                }
                else{
                    sum[i][j]=rowsum;
                }
//                cout<
            }
        }
        double res=n*cut(n,0,0,7,7)-sum[7][7]*sum[7][7];


        cout<<setprecision(3)<<fixed<<sqrt(res/(n*n))<<endl;
    }

    return 0;
}

17年模拟考

  1. To Europe!
    n辆车要通过一个窄桥,要分组,每组所用的时间是一个组里最慢的车所用的时间,一个组的重量不能超过桥的承重。前一组的车辆都排在后一组车辆的前面,即车辆的顺序是按输入固定的。需要用动态规划来解,状态是第i辆车过桥,它过桥的时候可以单独过桥也可以和前面的车一起过桥(只要不超重),最后时间就是这辆车所在队列的过桥时间+队列前那辆车过桥的最短时间,然后去找状态的最优解,解到最后n的最优值就是答案啦。
#include
#include
#include
#include
using namespace std;


int b,l,n;
int w[1005];
int s[1005];
double res[1005];
int main(){
	while(true){
		cin>>b>>l>>n;
		//cout<
		if(b==0 && l==0 && n==0){break;}
		l=l*60;
		for(int i=1;i<=n;i++){
			cin>>w[i]>>s[i];
			
		}
		res[0]=0.0;
		for(int i=1;i<=n;i++){
			int minv=s[i];
			int load=w[i];
			res[i]=1.0*l/s[i]+res[i-1]; //单独过桥
			for(int j=i-1;j>=1;j--){
				if(load+w[j]>b)break;
				else{
					minv=min(s[j],minv);//速度在减小
					load+=w[j];
					//如果第j辆车到第i辆车都可以安排在一起并且时间更少的话更新
					if(res[i]>res[j-1]+1.0*l/minv){
						res[i]=res[j-1]+1.0*l/minv; 
					}
				}
			}
		}
		cout<<fixed<<setprecision(1)<<res[n]<<endl;	
	}
	//system("pause");
	return 0;
}
  1. Palindrome
    求一个字符串最少插入多少字符组成回文。参考最大公共子序列,类似的,计算i~j最少插入的个数。
    注意循环的遍历顺序,画一个依赖关系图以i为横坐标j为纵坐标,发现每个dp依赖于右边和下面,以及右下角,所以i应当从n-1开始更新(从大到小),j应当比i大并且从头开始更新(从小到大)。
#include
#include
#include
#include
using namespace std;

int n;
char str[5005];
int dp[5005][5005];
int main(){
	cin>>n;
	cin>>str;
	memset(dp,0,sizeof(dp));
	for(int i=n-1;i>=0;i--){
	//for(int i=0;i
		for(int j=i;j<n;j++){
			if(str[i]==str[j]){
				dp[i][j]=dp[i+1][j-1];
			}
			else{
				dp[i][j]=min(1+dp[i+1][j],1+dp[i][j-1]);
			}
		}
	}
	cout<<dp[0][n-1]<<endl;
	//system("pause");
	return 0;
}

LeetCode

Cherry Pickup
Predict the winner
maximum length of repeated subarray

你可能感兴趣的:(LeetCode,数据结构)