【笔试算法题】动态规划(线性dp、前缀和、差分、二维dp、背包问题&树形dp)

一、线性dp

问题:给定一个正整数组成的数组,取出一些数使得和尽可能大,要求不能取相邻的数。len  1e6

例如:3,2,6,9,1   取3+9=12

例如:2,4,6,8,9   取2+6+9=17

dp[i][0]取了第i个数的情况下,前i个数取完的最大值

dp[i][1]没取第i个数的情况下,前i个数取完的最大值

得到递推关系:

  • dp[i][0]=dp[i-1][1] +a[i]  //第i个数取了,必定要第i-1个数不取。
  • dp[i][1]=max(dp[i-1][0],dp[i-1][i]) //第i个数不取,第i-1个数可取可不取。

二、前缀和、差分

问题1:给定一个数组,多次询问[l,r]区间的总和是多少?要求做到O(1)询问

首先求出前缀和数组dp[i]  (前i个数的和);使用后缀和也一样。

for(i=1;i<=n;i++) dp[i]=dp[i-1]+a[i];

dp[r]-dp[l-1] //区间的总合

问题2:给定一个数组,多次操作,每次操作[l,r]区间每个数加x,操作结束后输出数组 q次操作

假如有一个数组 1 2 3 4 5 6 7 8 需要在[2,4]上加1 ==》1 3 4 5 5 6 7 8

1.首先给出一个全为0的数组dp[i]  0 0 0 0 0 0 0 0 ;

2.然后使dp[l]+=x;dp[r+1]-=x   ==》 0 1 0 0 -1 0 0 0

3.前缀和后 0 1 1 1 0 0 0。

2和3步骤之间可以操作q次。(这一系列操作就是差分)

三、二维dp

问题:给定一个矩阵,初始在左上角,每一步可以向右或者向下,问路径数字和的最大值。

dp[i][j]代表从左上角到(i,j) 路径最大值

dp[i][j]=max(dp[i-1][j],dp[i][j-1])+a[i][j]

1

2

3

5

2

4

2

4

8

1

4

7

8

4

11

2

5

7

9

3

四、背包

问题1:n个物品,每个物品有重量a[i]和价值b[i],背包限制总重量w,问放入背包的物品价值最大值多少?要求复杂度O(n*w)    01背包

设dp[i][j]   代表前i个物品中,取总重量为j的物品的价值最大值。

那么有方程:dp[i][j]=max(dp[i][j],dp[i-1][j-a[i]]+b[i])

解释如下:如果取第i个物品,那么“前i个物品中,取总重量为j的物品”这个状态一定是从“前i-1个物品中,取总重量为j-a[i]的物品”这个状态再取第i个物品达成的。

问题2:n个物品,每个物品重量a[i]。问能否取部分物品达成总重量正好为w?要求复杂度O(n*w)

这道题如果n的范围是20,那么可以直接用状压枚举或dfs来解决。但是如果n比较大呢?比如n是100,w是1000的情况。

我们不妨设dp[i]代表重量i能被选到。那么由前i-1个物品能选到的所有情况显然可以直接推出第i个物品的。

五、树形dp

给定一棵有根树,求每个节点的子树大小。   一次dfs + dp 就可以出来了

int dp[100010];  
int dfs(int x){
    int i,sum=a[x];
    for(i=0;i<g[x].size();i++){
       if(g[x][i]!=pr)
        sum+=dfs(g[x][i]);
    }
    dp[x]=sum;
    return sum;
}

六、算法题

1.不相邻取数  难度⭐⭐

小红拿到了一个数组。她想取一些不相邻的数,使得取出来的数之和尽可能大。你能帮帮她吗?(具体思路上面写的有)

示例1:

输入

4
2 6 4 1

输出

7

说明

取 6 和 1 即可
import java.io.*;
public class Main {
 
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        String[] arr = br.readLine().split(" ");
        int[] nums = new int[n];
        for (int i = 0; i < n; i++) {
            nums[i] = Integer.parseInt(arr[i]);
        }
        System.out.println(test(nums, n));
    }
    //打家劫舍
    public static int test(int[] nums, int n){
        if(n == 1){
            return nums[0];
        }else if(n == 2){
            return Math.max(nums[0], nums[1]);
        }
        //nums[1] 要init nums[3]时要用到
        nums[1] = Math.max(nums[0], nums[1]);
        for (int i = 2; i < n; i++) {
            nums[i] = Math.max(nums[i - 2] + nums[i], nums[i - 1]);
        }
        return nums[--n];
    }
}

这里的方法同样是开头的思想,只不过没有使用二维数组来区分取或者没取。

nums[i] = Math.max(nums[i - 2] + nums[i], nums[i - 1]);

nums[i]表示前i个数满足条件的最大和。nums[i - 2] + nums[i]代表取了;nums[i - 1]代表没取 (为什么会这么设计呢?因为我们保存的值只需要一个,即最大值。而二维数组保存了两种情况下对应的两个值,那我们为何不将两种值比较然后赋值呢?)

2.abb型子序列  难度⭐⭐⭐

知识点:后缀和

枚举每个字符a[i],对于这个字符而言,需要求后面取两个相同字符(和当前字符不等)的方案数。 预处理出每种字母的后缀和,对于当前字母str,遍历25种和str不同的字母对应的后缀和,每个贡献为

leafee 最近爱上了 abb 型语句,比如“叠词词”、“恶心心”

leafee 拿到了一个只含有小写字母的字符串,她想知道有多少个 "abb" 型的子序列
定义: abb 型字符串满足以下条件:

  1. 字符串长度为 3 。
  2. 字符串后两位相同。
  3. 字符串前两位不同。

输入描述:

第一行一个正整数 n

第二行一个长度为 n  的字符串(只包含小写字母)

1<=n<=10^5

 

输出描述:

"abb" 型的子序列个数。 

示例1:

输入

6
abcbcc

输出

8

说明

共有1个abb,3个acc,4个bcc

示例2

输入

4
abbb

输出

3
import java.io.*;
 
public class Main {
    public static void main(String[] args)throws IOException {
        long result = 0;
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        long n = Integer.parseInt(br.readLine());
        if (n < 3) {
            System.out.println(0);
            return;
        }
        char[] chars = br.readLine().toCharArray();
        //a-z字母出现的次数
        long[] sum = new long[26];
        //当前对应字母的前面多少个与此字母不同
        long[] dp = new long[26];
        for (int i = 0; i < n; i++) {
            //字母表的位置下标
            int index = chars[i] - 'a';
            //纪录了有多少个加上次字母后,可以形成abb的形式
            result += dp[index];
            //更新操作:加上前面的其他(非自身)的个数,来组成ab的形式
            dp[index] += i - sum[index];
            //对应个数+1
            sum[index]++;
        }
        System.out.println(result);
    }
}

这里使用的是动态规划的优化版

3.小红取数  难度⭐⭐⭐

知识点:dp

遇到这种题的思路: 首先第一个想法是,开一个dp数组,dp[i]代表取前i个数的最大值。 那么dp[i]怎么求呢?很明显,如果要取了第i个数a[i],满足和能被k整除,那么就有个限制:前i-1个数里,满足取的数之和除以k的余数为k-a[i]%k,之后加上a[i]才能正好被k整除。 所以一维数组是不够的,需要开二维数组:dp[i][j]代表前i个数里面,取一些数,相加的和除以k的余数为j的最大值。

这样就得到转移方程: dp[i][(j+a[i])%k]=max(dp[i-1][j]+a[i],dp[i-1][(j+a[i])%k]);

题目描述:

小红拿到了一个数组,她想取一些数使得取的数之和尽可能大,但要求这个和必须是 k 的倍数。
你能帮帮她吗? 

示例1:

输入

5 5
4 8 2 9 1

输出

20

说明

取后四个数即可

import java.io.*;
import java.util.*;
public class Main{
    public static void main(String[] args){
        Scanner in = new Scanner(System.in);
        int n=in.nextInt();
        int k=in.nextInt(),i,j;
        long[] a = new long[n+1];
        long[][] dp = new long[n+1][k+1];
        for(i=1;i<=n;i++)a[i]=in.nextLong();
        for(i=0;i<=n;i++){
            for(j=0;j<k;j++){
                dp[i][j]=-9999999999999999L;
            }
        }
        dp[0][0]=0;
        for(i=1;i<=n;i++){
            for(j=0;j<k;j++){
                //把余数为0到k-1的都选择性的加a[i]。
                dp[i][(int)((j+a[i])%k)]=Math.max(dp[i-1][j]+a[i],dp[i-1][(int)((j+a[i])%k)]);
            }
        }
        if(dp[n][0]<=0)System.out.println(-1);
        else System.out.println(dp[n][0]);
    }
}

4.宵暗的妖怪  难度⭐⭐⭐

知识点:dp

由于取区间只取中间的点,要求区间不能重叠,所以相当于限制取的所有点中,任意两点的距离不小于3。 所以可以效仿上次讲的例题,设dp[i][0]代表取第i个点,前i个数的最大和;dp[i][1]代表不取第i个点,前i个数的最大和(要注意第一个数和最后一个数永远不能取到)。

因此有dp转移方程: dp[i][0]=dp[i-2][0]+a[i]; dp[i][1]=max(dp[i-1][0],dp[i-1][1]);

露米娅作为宵暗的妖怪,非常喜欢吞噬黑暗。

这天,她来到了一条路上,准备吞噬这条路上的黑暗。

这条道路一共被分为 n 部分,每个部分上的黑暗数量为a_i。

露米娅每次可以任取 连续的 未被吞噬过的 三部分,将其中的黑暗全部吞噬,并获得中间部分的饱食度。

露米娅想知道,自己能获得的饱食度最大值是多少?

示例1:

输入

7
2 4 1 4 2 1 8

输出

6

说明

选择[2,4,1]和[4,2,1]这两段即可。饱食度为4+2=6。

示例2

输入

7
2 4 1 7 2 1 8

输出

7

说明

选择[1,7,2]这一段即可。饱食度为7。 
值得注意的是,若取两段进行吞噬,反而最多只能获得6的饱食度,并不是最大的。
import java.io.*;
import java.util.*;
public class Main{
    public static void main(String[] args)throws IOException{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n=Integer.parseInt(br.readLine()),i;
        //以防万一,都预留一点空间
        int[] a = new int[n+1];
        long[][] dp = new long[n+1][3];
        String[] arr = br.readLine().split(" ");
        for(i=0;i<n;i++)a[i]= Integer.parseInt(arr[i]);
        dp[1][1]=a[1];
        dp[2][1]=a[2];
        dp[2][0]=a[1];
        for(i=3;i<n-1;i++){
            dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]);
            dp[i][1]=Math.max(dp[i-3][0]+a[i],dp[i-3][1]+a[i]);
        }
        System.out.println(Math.max(dp[n-2][0],dp[n-2][1]));
    }
}

5.小红的树  难度⭐⭐

知识点:树形dp、dfs/bfs

在搜索的过程中,统计所有儿子的子树红点数量,设dp[i]为第i个节点的子树红色节点的数量。

那么转移方程为: dp[x]=(所有儿子子树数量之和)+cnt 其中若当前点为红点,cnt为1,否则cnt为0。

题目描述:

小红拿到了一棵有根树。根节点为1号节点。
所谓树,指没有回路的无向连通图。
现在小红想给一部分点染成红色。之后她有 q 次询问,每次询问某点的子树红色节点的个数。 

示例1:

输入

5
1 2 1 4
WRWRR
3
3
4
5

输出

0
2
1

说明

这棵树形状如上图。

可以发现,3号节点的子树没有红色节点。

4号节点的子树共有2个红色节点。

5号节点的子树共有1个红色节点。

import java.io.*;
import java.util.*;
public class Main{
    static int[] dp;
    static String str;
    static ArrayList<ArrayList<Integer> >g;
    public static void main(String[] args){
        Scanner in = new Scanner(System.in);
        int n=in.nextInt(),i;
        dp=new int[n+5];
        g=new ArrayList<>(n+10);
        for(i=0;i<=n;i++)g.add(new ArrayList<>());
        for(i=2;i<=n;i++){
            int x=in.nextInt();
            g.get(x).add(i);
        }
        str=in.next();
        dfs(1,-1);
        int q=in.nextInt();
        while(q>0){
            q--;
            int x=in.nextInt();
            System.out.println(dp[x]);
        }
    }
    static int dfs(int x,int pr){
        int i,sum=0;
        if(str.charAt(x-1)=='R')sum++;
        for(i=0;i<g.get(x).size();i++){
            if(g.get(x).get(i)!=pr)sum+=dfs(g.get(x).get(i),x);
        }
        dp[x]=sum;
        return sum;
    }
}

猜你喜欢

转载自blog.csdn.net/indeedes/article/details/123979015
今日推荐