游园安排【第十一届】【决赛】【B组】

文章介绍了如何解决寻找最长上升子序列的问题,分别给出了时间复杂度为O(N^2)和O(NlogN)的解决方案。在N^2的方法中,利用动态规划和字典序判断,而在NlogN的方法中,结合了二分查找和贪心策略。文章还提供了具体的代码实现,并提及了对输出序列的处理技巧。
摘要由CSDN通过智能技术生成

 

题目看上去很复杂,其实抽象一下很简单。就是找到将名字看为一个整体,然后对不同名字构成的字符串进行计算,找到最长上升子序列。但是这题的变化之处在于找到最长上升子序列之后还要输出这个序列,而且如果多个子序列的长度相同,那么输出字典序较少的一个。

这题我们首先得预处理一下,把不同的名字切分一下,然后存到一个数组中。这样就可以直接访问这个处理好的数组进行比较和求解了。

先写一下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

智慧游园微信小程序demo是一个基于微信平台的智慧游园应用的简化版本。该小程序旨在提供游园者一个更加便捷、优质的游园体验。 首先,该小程序的功能主要包括导航指引、景点介绍、活动预约等。游园者可以通过小程序了解到游园区内各个景点的详细介绍,了解其历史、文化等相关信息。同时,小程序还提供导航功能,帮助游园者在游园区内迅速找到目的地。除此之外,小程序还设有活动预约功能,用户可以通过小程序提前了解游园区内的活动信息,并进行在线预约,提前安排游园行程。 其次,小程序还提供一些增值服务功能。例如,游园者可以通过小程序进行餐饮预订,提前预约午餐或晚餐,并选择就餐时间和位置,省去了在游园区内找餐厅的麻烦。另外,小程序还提供景点导游服务,游园者可以通过小程序雇佣导游进行专业的解说,更好地了解景点的历史和文化背景。 此外,该小程序还设置了一些互动功能,游园者可以在小程序上上传自己游园的照片和视频,与其他游园者分享自己的游园体验,增加用户的互动交流。 总之,智慧游园微信小程序demo通过提供导览、预约、增值服务和互动等功能,旨在提升游园者的游园体验,让游园更加智慧化和便捷化。同时,该小程序还为游园区提供了更好的管理和运营手段,能更好地提高游园区的服务质量和效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值