题目看上去很复杂,其实抽象一下很简单。就是找到将名字看为一个整体,然后对不同名字构成的字符串进行计算,找到最长上升子序列。但是这题的变化之处在于找到最长上升子序列之后还要输出这个序列,而且如果多个子序列的长度相同,那么输出字典序较少的一个。
这题我们首先得预处理一下,把不同的名字切分一下,然后存到一个数组中。这样就可以直接访问这个处理好的数组进行比较和求解了。
先写一下o(n^2)的解法:但是这个解法只能过70%的样例。
我们使用dp进行最长上升子序列的求解,其中dp[i]就是以i结尾的子串中最长上升子序列的长度。
那么我们对字符串进行从头到尾的遍历,对于每个位置i的元素,我们可以这么判断:是否存在一个j<i使得i跟在j后面,而且满足最长上升子序列的特性?如果对于i之前的元素j进行枚举,发现num[i]>num[j],那么首先满足了上升的特性。接下来呢,怎么判断这种方式能够满足最长呢,那么我们就需要比较dp[i]和dp[j]+1的情况了。通过这种方法,我们就可以得到每个i对应的dp值。直觉上看来,相当于前面的人已经找好了各自的上升队伍,那么当前的同学只需要找到前面比自己矮的人中所在的最长的队伍,然后跟在她身后就好了。
那么这个最长上升子序列的长度就能求出来了。但是题目还需要我们输出这个序列,这怎么办呢?我们只需要在转移的同时记录一下当前元素i前面跟着的元素就好了。如果dp[i]<dp[j]+1修改了,那么i前面跟着j。如果dp[i]=dp[j]+1,那么此时就要判断i当前的前一个元素和j的字典序哪个大了。如果j比较大,那么就保持不变,如果j比较小,那么就把i前面的元素改成j。
这样全部转移完之后,我们其实可以得到一个pre数组,这个数组记录了所有元素之前的元素。如果数组的值为0,说明这个元素就是子序列中打头的。那么我们最后输出序列值的时候通过不断迭代pre[i]就可以输出了。
最后,因为我们需要找到最长的上升子序列,所以我们得及时判断当前的上升子序列的值。这个判断要同时根据长度和字典序进行判断,长度大的、字典序小的胜出。
代码如下:
const int N = 1e6+5;
const int mod = 1e9+7;
vector<string>num; //将名字切分后的数组
int n;
vector<int>dp(N);
vector<int>pre(N);
bool check(int i, int j){
if(dp[i] != dp[j]) return dp[i] < dp[j];
string t1 = "";
string t2 = "";
while(i){
t1 = num[i] + t1;
i = pre[i];
}
while(j){
t2 = num[j] + t2;
j = pre[j];
}
return t1 > t2;
}
int main(){
string s;
cin>>s;
string tmp = "";
for(int i = 0; i < s.size(); i++){
if(s[i] >= 'A' && s[i] <= 'Z'){
num.push_back(tmp);
tmp = "";
}
tmp += s[i];
}
num.push_back(tmp);
n = num.size();
// for(int i = 1; i < n; i++) cout<<num[i]<<" ";
int ans = 0;
for(int i = 1; i < n; i++){
dp[i] = 1;
for(int j = 1; j < i; j++){
if(num[i] > num[j]){ //保证上升
if(dp[i] < dp[j] + 1){
pre[i] = j;
dp[i] = dp[j] + 1;
}else if(dp[i] == dp[j] + 1){
if(num[pre[i]] > num[j]){ //保证字典序最小
pre[i] = j;
}
}
}
}
if(check(ans, i)) ans = i; //保存最合理的子序列
}
string res = "";
// cout<<ans<<endl;
while(ans){ //不断找到之前跟着的元素,然后输出最后的序列
res = num[ans] + res;
ans = pre[ans];
}
cout<<res<<endl;
return 0;
}
//WoAiLanQiaoBei
这就是O(N^2)的方法,但是后三个样例会超时。
下面介绍O(NlogN)的方法。这个方法的核心是二分+贪心。
我们使用dp数组表示当最长上升子序列长度为i时,这个子序列末尾元素的最小值。
我们这么理解,同样长度的子序列,是不是末尾的人越矮,后面可以接着排队的人越多呢?所以这个就是贪心的精髓。
本来一个队都是2个人,结果先排好的队伍中末尾是1.4米的人,后排好的队伍中末尾的人是1.3米,那么我们保留那一队呢?当然是1.3米那一队,因为这样后面的人才好更多地往1.3米那一队后面接着排。反映到我们的题目中就是:
Wo Ai Lan Qiao Bei
dp[1] = "Ai" -> len = 1
dp[2] = "Bei" -> len = 2 Ai Bei
dp[3] = "Qiao" -> len = 3 Ai Lan Qiao
这个是怎么得到的呢?我们枚举每个位置的元素,然后进行判断。
当我们枚举到
i = 1,dp[1] = "Wo"
i = 2,发现"Ai"不能直接跟在"Wo"后面,所以需要在已经计算好的dp数组中找到比Ai小的最后一个值,使dp[i−1]<"Ai"<dp[i] 的下标 i,并更新 dp[i]=nums[j]。dp[1] = "Ai"
i = 3,发现"Lan"可以直接跟在dp[1]="Ai"后面,那么此时最长上升子序列的长度变大,dp数组引入新成员,dp[2] = "Lan".
i = 4,同理,dp[3] = "Qiao"
i = 5,发现不能直接跟在当前的最长上升子序列后面了,那么就得找到小于Bei的最后一个位置,将它后面的值替换为Bei。dp[2] = "Bei"
这么一番遍历下来,我们就可以找到不同长度的上升子序列的末尾元素的最小值了。
但是小于num[j]的最后一个元素怎么找呢?自己写二分吗还是使用现成的工具呢。我们想到了lower_bound可以用于查找大于等于n的第一个元素,那么大于等于n的元素的前一位不就是小于n的最后一个元素了吗。我们要修改的其实就是这个大于等于n的第一个元素,所以直接对函数的返回结果进行修改就好了。
那么就可以结合上面记录不同序列中字符的前一个位置的元素的方式,进行本题的求解了。
其中pos表示的是不同长度子序列的末尾元素在字符串数组中对应的位置。pre表示的是每个名字前面的一个名字。
这题我们在进行输出的时候,我用了res = num[i]+res的形式,但是最后一个样例还是一直超时。所以我用了题解的输出方式,找到一个数组把每个名字后面跟着的名字找出来,然后从头开始输出就行。
代码如下:
string num[N]; //将名字切分后的数组
int n = 0;
string dp[N];
int pos[N];
int pre[N];
int g[N];
int main(){
ios::sync_with_stdio(0);
string s;
cin>>s;
string tmp = "";
for(int i = 0; i < s.size(); i++){
if(s[i] >= 'A' && s[i] <= 'Z'){
num[++n] = tmp;
tmp = "";
}
tmp += s[i];
}
num[++n] = tmp;
// n = num.size();
int len = 0; //最长上升子序列的长度
int k = 0; //最长的子序列的位置
for(int i = 1; i <= n; i++){
int p = upper_bound(dp + 1, dp + 1 + len, num[i]) - dp - 1; //相对于dp[1]的位置
if(p + 1 >= len){ //其实不就是没找到比num[i]小的数吗
k = i;
len = p + 1;
}
dp[p + 1] = num[i]; //其实就是修改比num[i]小的最后一个元素的下一个元素
pos[p + 1] = i; //修改这个子序列末尾元素对应的字符串数组中的位置
pre[i] = pos[p]; //调整一下前缀
}
for(int i = k; i; i = pre[i])
{
g[pre[i]] = i;//建立反向的链表关系,准备输出结果
}
for(int i = 0; i != k; i = g[i]) //注意截至条件
{
cout << num[i];
}
cout << num[k];
return 0;
}
//WoAiLanQiaoBei
好了,两种方法就是这样了,感觉启发还挺大的。
下面附一下我看的几个题解,感谢各位大佬们!
【蓝桥杯】历届真题 游园安排【第十一届】【决赛】【B组】_宇佐美みずき的博客-CSDN博客
蓝桥杯 游园安排【第十一届】【决赛】【A组】LIS 路径还原 二分-bisect python-pudn.com
好了,就写到这里,到点了,回宿舍睡觉了!我也太困了Zzzz