我的天!!! 0.0 动态规划 T.T 记录一下,记得常来看看

从刷到一个动态规划的题说起,当然,我没做出来 -_-

package niukewang;

import org.junit.jupiter.api.Test;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/*
题目描述
计算 最少 出列多少位同学,使得剩下的同学排成合唱队形

说明:
N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学排成合唱队形。
合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1,2…,K,他们的身高分别为T1,T2,…,TK,
则他们的身高满足存在 i(1<=i<=K) 使得T1<T2<......<Ti-1 < Ti > Ti+1>......>TK。
你的任务是,已知所有N位同学的身高,计算 最少 需要几位同学出列,可以使得剩下的同学排成合唱队形。


注意:不允许改变队列元素的先后顺序 且 不要求最高同学左右人数必须相等
请注意处理多组输入输出!

备注:
1<=N<=3000

输入描述:
有多组用例,每组都包含两行数据,第一行是同学的总数N,第二行是N位同学的身高,以空格隔开

输出描述:
最少需要几位同学出列

示例1
输入
8
186 186 150 200 160 130 197 200

输出
4

说明
由于不允许改变队列元素的先后顺序,所以最终剩下的队列应该为186 200 160 130或150 200 160 130

本题知识点: 动态规划 队列
* */
public class test24 {
    public static void main(String[] args) {
        /*
        *   通过画出折线图进行分析 --- 动态规划问题???
        * */

    }
}


class Main {
    public static void main(String[] args) {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String str;
        try {
            while ((str = br.readLine()) != null) {
                if (str.equals("")) continue;

                //同学的总数N
                int n = Integer.parseInt(str);

                //记录N位同学的身高
                int[] heights = new int[n];
                String[] str_heights = br.readLine().split(" ");

                // 当仅有一个人时,其自己组成一个合唱队,出列0人
                if (n <= 1) System.out.println(0);

                // 当 n > 1时
                else {
                    for (int i = 0; i < n; i++) heights[i] = Integer.parseInt(str_heights[i]);

                    // 记录从左向右的最长递增子序列seq 和 从右向左的最长递增子序列rev_seq
                    int[] seq = new int[n], rev_seq = new int[n];

                    // 用于记录以i为终点的从左向右和从右向左的子序列元素个数
                    /*
                    *   记录的是遍历到的当前值放入seq(或rev_seq)中后,它左边的元素个数,即比它小的元素个数
                    * */
                    int[] k = new int[n];


                    //先找出从左到右递增的子序列
                    seq[0] = heights[0]; // 初始化从左向右子序列首元素为第一个元素
                    int index = 1; // 记录当前子序列seq的长度
                    for (int i = 1; i < n; i++) { //注意: 下标从1开始
                        if (heights[i] > seq[index-1]) {  // 当当前元素大于递增序列seq最后一个元素时
                            k[i] = index;  // 其左边元素个数
                            seq[index++] = heights[i];  // 更新递增序列seq
                        } else {  // 若当前元素位于目前维护递增序列seq之间
                            // 使用二分搜索找到其所属位置
                            int l = 0, r = index - 1;
                            // 在子序列seq中找到当前值应该出现的位置,并用它替换子序列seq中那个位置上的值
                            while (l < r) {
                                int mid = l + (r - l) / 2; // int mid = (l + r) / 2;
                                if (seq[mid] < heights[i]) l = mid + 1;
                                else r = mid;
                            }
                            seq[l] = heights[i];  // 将所属位置值进行替换
                            k[i] = l;  // 其左边元素个数
                        }
                    }

                    // 随后,再从右向左进行上述操作
                    rev_seq[0] = heights[n-1];  初始化从左向右子序列首元素为最后一个元素
                    index = 1; // 记录当前子序列rev_seq的长度
                    for (int i = n - 2; i >= 0; i--) { //下标从 n-2 开始,因为现在是在找从右向左递增的子序列
                        if (heights[i] > rev_seq[index-1]) {
                            k[i] += index;
                            rev_seq[index++] = heights[i];
                        } else {
                            int l = 0, r = index - 1;
                            while (l < r) {
                                int mid = l + (r - l) / 2;
                                if (rev_seq[mid] < heights[i]) l = mid + 1;
                                else r = mid;
                            }
                            rev_seq[l] = heights[i];
                            k[i] += l;
                        }
                    }

                    //通过比较数组k中元素大小,计算最少出列人数
                    int max = 1;
                    for (int num: k){
                        if (max < num) max = num;
                    }

                    // max+1为最大的k,即符合条件的合唱队形的人数最多有k人,则最少应当出列 n - (max + 1)人
                    System.out.println(n - max - 1);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

然后,突然想到这题好像在我之前看过的一个动态规划刷题视频中出现过,最后发现并不是一样的题

    /*
    *   动态规划问题创建dp[i]数组,并给它赋相应的值,以及i代表的状态(前i个,还是以第i个为结尾等)很关键
    *
    *   第一种思路:
    *       通过数组dp[]一下就可以得到答案;
    *       将整个大的问题拆成一个个小的问题推导,dp[i]就代表有i阶楼梯、有i个房子等,从而推导出最终的dp[n]
    *   第二种思路:
    *       通过数组dp[]可以获取中间结果,再通过对dp[]进行进一步简单的操作后,才能得到最终答案
    * */

    /*
    * 爬楼梯问题
    * */
    @Test
    void test1(){
        //台阶总阶数
        int total = 4;
        System.out.println("总共有 " + climbStairs(total) + " 种爬法!");
    }
    public static int climbStairs(int total){
        /*
        *  dp[i]代表当有i阶楼梯的时候,有多少中爬法
        * */
        int[] dp = new int[total+3];
        dp[1] = 1;
        dp[2] = 2;
        for (int i = 3; i <= total; i++) {
            dp[i] = dp[i-1] + dp[i-2];
        }

        return dp[total];
    }


    /*
    * 打家劫舍问题
    * */
    @Test
    void test2(){
        int[] arr = {5, 2, 6, 3, 1, 7};
        System.out.println("最多可盗取金额:  " + rob(arr));
    }
    public static int rob(int[] arr){
        int len = arr.length;
        if (len == 0) return 0;
        else if (len == 1) return 1;
        else if (len == 2) return Math.max(arr[0], arr[1]);

        /*
        *  dp[i]代表当有i间房屋供选择时,最多可盗取的金额
        * */
        int[] dp = new int[len];
        dp[0] = arr[0];
        dp[1] = Math.max(arr[0], arr[1]);
        for (int i = 2; i < len; i++) {
            dp[i] = Math.max(dp[i-1], dp[i-2] + arr[i]);
        }

        return dp[len-1];
    }


    /*
    * 最大连续子数组和问题
    * */
    @Test
    void test3() throws Exception {
        int[] arr = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
        System.out.println("最大连续子数组和为: " + maxSubArray(arr));
    }
    public int maxSubArray(int[] arr) throws Exception {
        int len = arr.length;

        if (len == 0) throw new Exception("数组长度不可为0");
        else if (len == 1) return arr[0];

        /*
        *   dp[i]代表以第i个元素结尾的最大子数组的和
        * */
        int[] dp = new int[len];
        dp[0] = arr[0];
        int max = dp[0];
        for (int i = 1; i < len; i++) {
            if (dp[i-1] > 0) dp[i] = dp[i-1] + arr[i];
            else dp[i] = arr[i];

            if (dp[i] > max) max = dp[i];
        }

        return max;
    }


    /*
    * 找零钱问题 -- 动态规划;背包问题中的一种
    * */
    @Test
    void test4(){
        int[] coins = {1, 2, 5, 7, 10}; //需要升序数组???不需要
        int amount = 14;
        System.out.println("找零成功,至少需要 " + coinChange(coins, amount) + " 张来换");
    }
    public int coinChange(int[] coins, int amount){
        /*
        *  dp[i]代表金额为i时,至少需要几张来换
        * */
        int[] dp = new int[amount+1];

        //使用Arrays工具类给数组赋初值
        Arrays.fill(dp, -1);
        dp[0] = 0;

        //注意: dp[]下标从1开始,dp[0]永远保持为0
        for (int i = 1; i <= amount; i++) {
            //循环各个面值,找到dp[i]的最优解
            for (int coin : coins) {
                /*
                * dp[i - coin] != -1 代表金额为i,可以用面值为coin来换;即金额为i-coin的最优解为dp[i-coin]
                * */
                if ((i - coin >= 0) && (dp[i - coin] != -1)) {
                    if (dp[i] == -1 || dp[i] > dp[i - coin] + 1) {
                        dp[i] = dp[i - coin] + 1;//背包问题中的一种
                    }
                }
            }
        }

        return dp[amount];
    }


    /*
    *  最长上升子序列长度问题 -- "子序列"不要求连续
    * */
    @Test
    void test5(){
        int[] arr = {1, 3, 2, 3,  1, 4};
        System.out.println("最长上升子序列长度: " + lengthOfLIS2(arr));
    }
    /*
     *   思路1: 动态规划
     * */
    public int lengthOfLIS(int[] arr){
        if (arr.length == 0) return 0;

        /*
        * dp[i]代表以第i个元素作为子序列尾部的最长上升子序列的长度
        *   则arr[i]一定是dp[i]对应的最长上升子序列中的最大者(因为在末尾,序列又是上升的)
        * */
        int[] dp = new int[arr.length];
        dp[0] = 1;

        int max = 1;
        for (int i = 1; i < dp.length; i++) {
            dp[i] = 1; //当第i个元素左边的都比它大的时候,那么以它自己为最长子序列尾部时,最长子序列长度为1
            for (int j = i - 1; j >= 0; j--) {
                if (arr[i] > arr[j]) {
                    if (dp[j] + 1 > dp[i]) {
                        dp[i] = dp[j] + 1;
                    }
                }
            }
            if (dp[i] > max) max = dp[i];
        }

        return max;
    }
    /*
     *   思路2: 逐个取出数组arr中的元素,按照一定规则添加进list集合中,最终list集合的长度就是想要的结果
     *          添加规则为: 1. 如果待添加的元素大于list的“栈顶”,直接添加进list
     *                      2. 否则,“从栈底到栈顶”找到第一个 大于等于 待添加元素的位置,然后用待添加元素替换该位置上的元素
     * */
    public int lengthOfLIS2(int[] arr){
        if (arr.length <= 1) return arr.length;

        List<Integer> list = new ArrayList<>();
        list.add(arr[0]);

        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > list.get(list.size()-1)) list.add(arr[i]);
            else {
                //使用二分查找,找到合适的插入位置
                int pos = binary_search(list, arr[i]);
                list.set(pos, arr[i]);
            }
        }

        return list.size();
    }
    /*
    *   二分查找
    * */
    public int binary_search(List<Integer> list, int target){
        int index = -1;
        int begin = 0;
        int end = list.size();

        while (index == -1){
            int mid = (begin + end) / 2;
            if (target == list.get(mid)) index = mid;
            else if (target < list.get(mid)) {
                if (mid == 0 || target > list.get(mid - 1)) {
                    index = mid;
                }
                else end = mid - 1;
            }
            else if (target > list.get(mid)) {
                if (mid == list.size() - 1 || target < list.get(mid + 1)) {
                    index = mid + 1;
                }
                else begin = mid + 1;
            }
        }

        return index;
    }

最后,通过绞尽脑汁,我想出动态规划解法???

  1. 突然发现,这和上面那道“找最长上升子序列长度”题,思想一样;
  2. 这题只需要从左往右dp1[i]、从右往dp2[i]左两个方向,找到以第i个元素为末尾的最长上升子序列的长度
  3. 最后,只需要将两个dp数组对应位置相加,再减1,就可以得到最终的以第i个元素为合唱队的中间位置的最长子序列的长度
  4. 注意: 但是,很容易发现,dp1[i]和dp2[i]重复考虑并计算了第i个元素,所以,最终的长度应该为: dp1[i] + dp2[i] - 1
  5. 既然合唱队形的最长长度计算出来了,那么最少出列的同学位数只需用总人数减去合唱队形的最长长度,即: n - (dp1[i] + dp2[i] - 1)
import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        /*
        *   通过画出折线图进行分析 --- 动态规划问题???
        *
        *   先计算出符合合唱队形最多多少人,再用总人数减去它,就可以得出最少出列多少人
        * */
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String input;

        try {
            while ((input = br.readLine()) != null) {
                int n = Integer.parseInt(input);
                if (n <= 1) {
                    System.out.println(0);
                    continue;
                }

                String line = br.readLine();
                String[] split = line.split(" ");



                /*
                *   思路:  先求从左到右最长上升子序列,dp[i]代表以第i个数位末尾的最长上升子序列的最大长度
                *           再求从右到左最长上升子序列,dp[i]代表以第i个数位末尾的最长上升子序列的最大长度
                * */


                /*
                *   从左到右找最长上升子序列
                * */
                int[] dp = new int[n];
                Arrays.fill(dp, 1);//先得给dp[i]赋值为1,表示只有他自己的上升子序列最长长度为1
                for (int i = 1; i < n; i++) {
                    for (int j = i - 1; j >= 0; j--) {
                        if (Integer.parseInt(split[i]) > Integer.parseInt(split[j])) {
                            if (dp[j] + 1 > dp[i]) {
                                dp[i] = dp[j] + 1;
                            }
                        }
                    }
                }


                /*
                *   从右到左找最长上升子序列
                * */
                int[] dp2 = new int[n];
                Arrays.fill(dp2, 1);
                for (int i = n - 2; i >= 0; i--) {//遍历dp[]
                    for (int j = i + 1; j < n; j++) {//遍历split[]
                        if (Integer.parseInt(split[i]) > Integer.parseInt(split[j])) {
                            if (dp2[j] + 1 > dp2[i]) {
                                dp2[i] = dp2[j] + 1;
                            }
                        }
                    }
                }

                /*System.out.println(Arrays.toString(dp));
                System.out.println(Arrays.toString(dp2));*/

                int max = 1;
                for (int i = 0; i < n; i++) {
                    if (dp[i] + dp2[i] > max) max = dp[i] + dp2[i];
                }

                /*
                *   注意: 从左到右、从右到左,都将第i个元素计算在内了,所以重复计算了,需要max-1,才得到最终的合唱队最长长度
                * */
                //System.out.println(max - 1);//合唱队形最长长度
                System.out.println(n - (max - 1));//最少出列人数

            }
        } catch (IOException e) {
            e.printStackTrace();
        }


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值