【2024.10.28练习】游园安排

题目描述


题目分析

这是一道用DP解决的最长上升子序列(LIS)问题。

时间复杂度为O(n^2)的解法:

定义dp[i]为“以name_i为末尾的最长上升子序列长度”,状态转移方程为:

dp[i]=max\left \{ 1,dp[j]+1 \ | \ j<i,name_j<name_i \right \}

由于这种递推方法需要往回看字典序比自己小的名字,因此在本题中采用会超时。


时间复杂度为O(n*logn)的解法:

定义dp[i]为“长度为i+1的上升子序列中末尾元素的最小值”(不存在的话字符串为十个Z以确保最大)

状态转移方程为:

dp[i]=min\left \{ dp[i],name_j \ |\ i=0\cup name_j>dp[i-1]\right \}

由于对于每个name_j,都要从0到i的dp数组更新一遍,因此时间复杂度仍然为O(n^2)。

但dp数组在这里的特点是有序递增,因此可以用二分查找优化算法。

使用该算法得到的结果子序列就是从第一个名字开始依次字典序最小的方案。


求出具体子序列:

要实现该动态规划的回溯很简单,定义一个前驱数组pre[i]即可,该数组记录了name_i作为某条子序列末尾元素时上一个元素的下标。


我的代码

注意题目细节:游客可能重名,而子序列又要求前一个必须严格小于后一个,因此通过upper_bound()函数找到dp[i]时注意看dp[i-1]是否等于当前名字,若是则不更新dp[i]的值;而使用lower_bound()就不会出现此问题。

#include<iostream>
#include<algorithm>
#include<cmath>
#include<stack>
using namespace std;
string s;
string name[1000002];
string dp[1000002];
int dp_2[1000002];
int pre[1000002];
int N;
void init(){
	for(int i=0;i<N;i++){
		dp[i]="zzzzzzzzzzz";
		pre[i]=0x3f3f3f3f;
	}
}
int split(string str){
	int n=0;
	name[n]+=str[0];
	for(int i=1;i<str.length();i++){
		if(str[i]<='Z'&&str[i]>='A') n++;
		name[n]+=str[i]; 
	}
	return (n+1);
}
int main(void)
{
	cin>>s;
	N=split(s);
	init(); 
	//Binary Search
	for(int i=0;i<N;i++){
		int pos = lower_bound(dp,dp+N,name[i])-dp;
		dp[pos]=name[i];
		dp_2[pos]=i;
		if(pos!=0) pre[i]=dp_2[pos-1];
	} 
	//Ourput Answer
	int t=0;
	for(int i=0;i<N;i++){
		if(dp[i]=="zzzzzzzzzzz"){
		t=dp_2[i-1];
		break;	
		}
	}
	stack<int> ans;
	ans.push(t);
	while(pre[t]!=0x3f3f3f3f){
		t=pre[t];
		ans.push(t);
	}
	while(ans.size()){
		cout<<name[ans.top()];
		ans.pop();
	}
	return 0;
}


思路扩展

这里为了求出最长子序列建立了前驱数组。但也可以建立一个ans[i]字符串数组,代表了长度为i+1的最小字典序子序列。状态转移方程如下:

每当dp[i]更新时,若i=0,ans[i]=name[i];若i>0,ans[i]=ans[i-1]+name[i]。最终输出结果直接输出最大i对应的ans[i]即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值