游园安排
这道题我在一年前做过,但是当时其实对很多算法概念并不是很清楚,今年又侥幸进入了国赛,所以回来温习一下这道题,但是发现竟然自己之前写的题解有点看不懂了😅
所以今天,我重新理解了一下去年做的题目,然后写下了这篇也许会稍微易懂一些的博客
题目描述
L 星球游乐园非常有趣,吸引着各个星球的游客前来游玩。小蓝是 L 星球游乐园的管理员。
为了更好的管理游乐园,游乐园要求所有的游客提前预约,小蓝能看到系 统上所有预约游客的名字。每个游客的名字由一个大写英文字母开始,后面跟 0 个或多个小写英文字母。游客可能重名。
小蓝特别喜欢递增的事物。今天,他决定在所有预约的游客中,选择一部 分游客在上午游玩,其他的游客都在下午游玩,在上午游玩的游客要求按照预 约的顺序排列后,名字是单调递增的,即排在前面的名字严格小于排在后面的 名字。
一个名字 A 小于另一个名字 B 是指:存在一个整数 i,使得 A 的前 i 个字母与 B 的前 i 个字母相同,且 A 的第 i + 1 i+1 i+1个字母小于 B 的第 i + 1 i+1 i+1个字母。(如果 A 不存在第 i+1 个字母且 B 存在第 i+1 个字母,也视为 A 的第 i+1 个字母小于 B 的第 i + 1 i+1 i+1个字母)
作为小蓝的助手,你要按照小蓝的想法安排游客,同时你又希望上午有尽 量多的游客游玩,请告诉小蓝让哪些游客上午游玩。如果方案有多种,请输出 上午游玩的第一个游客名字最小的方案。如果此时还有多种方案,请输出第一 个游客名字最小的前提下第二个游客名字最小的方案。如果仍然有多种,依此 类推选择第三个、第四个……游客名字最小的方案。
输入描述
输入包含一个字符串,按预约的顺序给出所有游客的名字,相邻的游客名 字之间没有字符分隔。
其中有 ,每个名字的长度不超过 10 个字母,输入的总长度不超 过 1 0 6 10^6 106个字母。
输出描述
按预约顺序输出上午游玩的游客名单,中间不加任何分隔字符。
输入输出样例
示例
输入
WoAiLanQiaoBei
输出
AiLanQiao
运行限制
最大运行时间:1s
最大运行内存: 128M
解析
嗯。。。其实这道题最难的地方就是理解题目,现在蓝桥杯确实时越来越考察阅读理解水平了。。。
很多博主都说,这道题是一个求最长上升子序列的问题,但是没有说为什么是最长子序列的问题。
根据题目意思,我们其实是要从上面的人名中选择出部分人,让他们的名字是递增排序的,并且要保证选出的人数最多。
难就难在这部分理解,很多人会当成是一道
排序然后选出部分人在上午游园的题目,但是实际上他是一道选择部分人使得他们的名字是递增排序的。不要搞混咯那么接下来进入正题,还是和去年一样,先介绍比较容易想到的做法
最长上升子序列是啥?
上升序列指的是一段序列中,对于每个字符(串),它一定大于其左边的字符(串),并且小于右边的字符(串)
那么最长上升子序列其实就是说在一段序列中取出最多个字符,使之满足上升序列的定义
法1️⃣:动态规划
动态规划,我们主要需要找到初始状态和动态转移方程,还有一个难点就是dp数组下标代表的含义是什么。
我们的第一个方法就是使用动态规划。
First of all,我们需要先判断dp数组的下标代表的含义, d p [ i ] dp[i] dp[i]表示在字符串的 0 − ( i − 1 ) 0 \ - \ (i-1) 0 − (i−1)个字符组成的子串,并且以 i i i作为当前子串的目前最后一个字符的上升序列中最长的上升序列的长度。
Secondly,我们需要找到初始状态,这个也很容易找到,在一开始没有子串,那么最长上升子序列长度 d p [ 0 ] dp[0] dp[0]肯定是0咯
Thirdly,我们需要找到动态转移方程,首先我们既然知道了 d p [ i ] dp[i] dp[i]的值,那我们一定是知道 d p [ 0 − ( i − 1 ) ] dp[0-(i-1)] dp[0−(i−1)]的值的,既然如此我们一个个遍历前面那些状态中,末尾字符(串)(也就是str[0]-strp[i-1] )小于当前字符(串) s t r [ i ] str[i] str[i]的状态dp[j],找到最大的dp[j],那么此时将这个最长的字符串(串)(没写错!!!),也就是那个满足尾部小于当前字符(串)str[i]的那个最长子序列 ,给他加上我们的str[i],那么dp[i]的值就是max(dp[j])+1。
由此可以得到动态转移方程:
d p [ i ] = m a x ( d p [ j ] ) + 1 ( s t r [ j ] < s t r [ i ] ) dp[i]=max(dp[j])+1\ \ \ \ \ \ (str[j]<str[i]) dp[i]=max(dp[j])+1 (str[j]<str[i])
然后,之前我的那篇题解有人问逆序输出的问题,这里我回答一下为什么逆序可以找到最长子序列
首先根据上面的dp数组的含义,我们可以知道最后一个dp[length-1]不一定是最长子序列,因为最长子序列不一定会包含最后字符(串)
那么我们想要回溯找到这个子串的全部字符(串),就需要从最长的这个子串的最后一个字符进行遍历,因此我们在dp数组更新的过程中还需要记录最长子串的下标。
那么我们在回溯过程中,就有点像一个树,不断往上寻找父节点
最长子序列一定是由str[max_index]这个字符(串)和 s t r [ j ] ( 0 ≤ j ≤ m a x _ i n d e x , d p [ j ] = d p [ m a x _ i n d e x ] − 1 ) str[j]\ \ \ (0\leq j\leq max\_index,dp[j]=dp[max\_index]-1) str[j] (0≤j≤max_index,dp[j]=dp[max_index]−1)为最后一个字符的子串构成的,那么这个 s t r [ j ] str[j] str[j]就可以看成他的父节点,一步步网上回溯,就可以找到整个子串了。
好的,到此就只剩代码了,由于今年蓝桥杯报名的是java组,所以代码使用java编写
package com.lanqiao;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Scanner;
/**
* @author 王宇哲
* @date 2022/5/24 15:51
*/
public class 游园安排 {
public static void main(String[] args) {
//获取输入的任名,并将他们转换成字符串列表存储,方便后续操作
Scanner scanner = new Scanner(System.in);
String namesStr = scanner.next();
char[] names = namesStr.toCharArray();
int cnt = 0;
ArrayList<Name> name = new ArrayList<>();
while (cnt < names.length) {
StringBuilder tmp = new StringBuilder("" + names[cnt++]);
while (cnt < names.length && 'a' <= names[cnt] && names[cnt] <= 'z') {
tmp.append(names[cnt++]);
}
name.add(new Name(tmp.toString()));
}
//dp[i] 表示0-i个人中,最多的游园人数
int[] dp = new int[name.size()];
//存储最长子序列的最后一个名字的下标
cnt = 0;
//存储最长子序列的长度
int maxL = -1;
for (int i = 0; i < name.size(); i++) {
dp[i] = 1;
//从后往前和从前往后遍历结果一样
for (int j = i-1; j >= 0; j--) {
//如果当前名字j小于名字i,则说明i名字可以放在j名字后面,那就比较dp[j]和dp[i]的大小,取最大值
if (name.get(i).compareTo(name.get(j)) > 0) {
dp[i] = Math.max(dp[i],dp[j]+1);
}
}
//更新最长子序列的长度与其最后一个名字的下标
if (dp[i]>=maxL){
maxL = dp[i];
cnt = i;
}
}
//从后往前遍历,回溯找出最长子序列
StringBuilder result = new StringBuilder();
for (int i = cnt;i>=0;i--){
//找到父节点,添加到结果中
if(maxL == dp[i]){
result.insert(0,name.get(i).name);
//更新一下要找的结点
maxL--;
}
}
System.out.println(result.toString());
}
}
/**
* 名字类
* @author 王宇哲
* 可有可无,主要是为了方便比较名字大小,继承了Comparable接口
* 有点多此一举
*/
class Name implements Comparable<Name> {
String name;
public Name(String name) {
this.name = name;
}
@Override
public int compareTo(Name o) {
return this.name.compareTo(o.name);
}
}
法2️⃣:二分搜索+贪心策略
这个策略还是比较难想到的,比起上面那个,这个完全是一个考察能力的方法
简而言之,我们发现在上面的动态规划中,有用的东西最后只有两个,一个是以每个字符(串)为最后一位的序列的最长长度,还有一个是最长子序列的最后一个字符(串)。
那我们直接想办法只求这两个东西,就能优化效率了。
于是我们使用贪心的策略,对于每个字符(串),如果它比序列队列中的尾部大,那么就将其加入队尾,此时队列作为一个上升序列长度也会大1。
但是,如果它比序列队列中尾部小,就用它替换掉队列中比它大的最小的元素,这样做是为了解决法1️⃣中嵌套循环的问题,这样子可以保证序列中的元素都是靠后且更小的元素(有点像操作系统中的最近最久未使用算法),这样就能减少寻找的次数,即我们每次都是用最大的满足要求的字符(串)对dp数组进行更新的
这样就能优化效率了
回溯法虽然长得不一样,但是跟上面的原理一样。还有我自己写了一个用于二分查找的工具类,类似去年的博客的
lower_bound
package com.lanqiao;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.*;
/**
* @author 王宇哲
* @date 2022/5/25 19:19
*/
public class 游园安排_2 {
public static void main(String[] args) throws Exception {
//使用缓存输入,提高速度
//获取名字存储在数组中
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String namesStr = br.readLine();
char[] names = namesStr.toCharArray();
int cnt = 0;
ArrayList<String> name = new ArrayList<>();
while (cnt < names.length) {
StringBuilder tmp = new StringBuilder("" + names[cnt++]);
while (cnt < names.length && 'a' <= names[cnt] && names[cnt] <= 'z') {
tmp.append(names[cnt++]);
}
name.add(tmp.toString());
}
//相当于dp数组,存储当前下标的最长子串长度
ArrayList<Integer> index = new ArrayList<>();
//value列表最后的长度是最长子串长度,最后一个元素是最长子串的最后一个字符(串)
//但是除了最后一个元素,其他元素不一定是最长上升子串中的元素
ArrayList<String> value = new ArrayList<>();
//index相当于dp数组,刚开始有一个字符串,长度为1
index.add(1);
//value先存放第一个字符串
value.add(name.get(0));
//从第二个字符串开始遍历
for (int i = 1; i < name.size(); i++) {
//如果当前字符串能够放到上一个字符串后面,则放到上一个字符串后面
if(name.get(i).compareTo(value.get(value.size() - 1)) > 0) {
value.add(name.get(i));
index.add(value.size());
continue;
}
//如果当前字符串不能放到上一个字符串后面,则寻找其能够替换的字符串
int j = MyCollection.binarySearch(value, name.get(i));
value.remove(j);
value.add(j, name.get(i));
index.add(j+1);
}
//使用StringBuilder拼接字符串,会更快
StringBuilder result = new StringBuilder();
cnt = value.size();
String[] res = new String[cnt];
for(int i = name.size()-1;cnt>0;i--) {
if(index.get(i) == cnt){
res[--cnt] = name.get(i);
}
}
for (String re : res) {
result.append(re);
}
System.out.println(result);
}
}
class MyCollection {
/**
* 查找指定元素的索引,找不到返回比它小的最大索引
* @param list 元素列表
* @param key 要找的元素
* @return 元素的索引
*/
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
int low = 0;
int high = list.size() - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
Comparable<? super T> midVal = list.get(mid);
int cmp = midVal.compareTo(key);
if (cmp < 0) {
low = mid + 1;
} else if (cmp > 0) {
high = mid - 1;
} else {
return mid;
}
}
return low;
}
}