从刷到一个动态规划的题说起,当然,我没做出来 -_-
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;
}
最后,通过绞尽脑汁,我想出动态规划解法???
- 突然发现,这和上面那道“找最长上升子序列长度”题,思想一样;
- 这题只需要从左往右dp1[i]、从右往dp2[i]左两个方向,找到以第i个元素为末尾的最长上升子序列的长度
- 最后,只需要将两个dp数组对应位置相加,再减1,就可以得到最终的以第i个元素为合唱队的中间位置的最长子序列的长度
- 注意: 但是,很容易发现,dp1[i]和dp2[i]重复考虑并计算了第i个元素,所以,最终的长度应该为: dp1[i] + dp2[i] - 1
- 既然合唱队形的最长长度计算出来了,那么最少出列的同学位数只需用总人数减去合唱队形的最长长度,即: 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();
}
}
}