超详细学习笔记:动态规划的时间优化(n*n -> n*logn)

luogu上刷到的P1020 [NOIP1999 提高组] 导弹拦截P1439 【模板】最长公共子序列 有感

LIS:Longest Increasing Subsequence,最长递增子序列

给定一个字符串,求出最长递减序列

这个题问的是下降,上升情况反过来就好了

只考虑第一问,由于O(n*n)会爆T(不解释了),考虑压缩时间

还记得在网上看到的一句话

如果需要对dp进行时间优化,不妨交换状态参数和状态量

基于这句话的启发,这个题思路就若隐若现了

步骤一:首先我们很容易想到 dp[ i ] 来表示:前i个数中以第i个数结尾的最长递减序列

这句话中我理解的状态参数就是(以第i个数结尾)状态量就是(最长递减序列

我们不妨构造 f 数组,f [ i ] 代表长度为 i 的递减序列的最后一个数的最大值

(为什么要怎么做,等会解释,先理解成最大值后面才能放更多小的数使序列更长)

这个时候状态参数就是(长度为 i 的递减序列)状态量就是(结尾的最大值

这样我们就实现了递减序列结尾数字的交换(前面可以再看一遍)

步骤二:需要一个前提证明,f 数组是单调递减的(其实是单调不递增,为了不拗口先这么叫):

反证一下,如果 f [x+1] > f [x] 也就是说长度为x+1的递减序列的末尾会大于长度为x的,那就不对了,由于序列递减,这个长度为x+1的序列的第x个数(称作a)一定>=第x+1个数(称作b),那长度为x的序列结尾的数至少就会跟这个a相等,也就是发 f [ x ] >=a,由于a>=b,b=f [ x+1 ],则 f [x] > f [x+1]得证。要是被绕了就看看这个例子:

长度为4的递减序列:9 6 3 2 ,如果长度为3的递减序列末尾比2还小就会冲突,因为这个序列至少可以取到9 6 3,这个最大值至少比3还大,也就不可能小于2了

步骤三:dp [ i ] 和 f [ i ] 同步进行计算,相互更新,计算dp[ i ] 需要对于每一个 i 满足:

想要用第i 个数结尾,前面的结尾必须比第i个数还大,但是前面满足的递减序列又要尽可能长,也就是i尽可能大,但是 f [ i ] 要比第i个数小,直接二分找到答案。(f 数组求dp数组)

每次计算完 dp [ i ] ,设这个长度为k, 也就是dp [ i ]=k,那么 f [ k ] 长度为k的递减序列的末尾就要尝试更新为第 i 个数。(dp 数组更新 f 数组)

最后遍历看看以谁结尾的最长递减序列最长就好了

AC代码如下(fun2函数是第二问的请忽略,后面的循环和ans数组也是):

//动态规划的时间优化 
#include<bits/stdc++.h>
#define N 100050
using namespace std;
typedef long long ll;
vector<ll>ans;
ll h[N],dp[N],f[N];
ll fun(ll i){
	ll l=0,r=N-1;
	while(l<r-1){
		ll mid=(l+r)/2;
		if(f[mid]>=h[i]){//等于的情况要特殊考虑!!! 
			l=mid;
		}else{
			r=mid;
		}
	}
	if(f[r]>=h[i] && h[i]){
		return r;
	}else{
		return l;
	}
}
void fun2(ll i){
	if(ans.size()==0){
		ans.push_back(i);
	}else if(ans.size()==1){
		if(ans[0]>=i){
			ans[0]=i;
		}else{
			ans.push_back(i);
		}
	}else{
		ll l=0,r=ans.size()-1;
		while(l<r-1){
			ll mid=(l+r)/2;
			if(ans[mid]>i){
				r=mid;
			}else{
				l=mid;
			}
		}
		if(ans[l]>=i){
			ans[l]=i;
		}else if(ans[r]>=i){
			ans[r]=i;
		}else{
			ans.push_back(i);
		}
	}
}
int main(){
	ll n=1;
	while(cin>>h[n]){
		n++;
	}
	n--;
	dp[1]=1;
	f[1]=h[1];
	for(int i=2;i<=n;i++){
		ll k=fun(i);
		dp[i]=k+1;
		f[dp[i]]=h[i];
	}
	ll temp_max=0;
	for(int i=1;i<=n;i++){
		temp_max=max(temp_max,dp[i]);
	}
	cout<<temp_max<<endl;
	for(int i=1;i<=n;i++){
		fun2(h[i]);
	}
	cout<<ans.size();
	return 0;
}

LCS:Longest Common Subsequence,最长公共子序列

最长公共子序列模板(nlogn优化) 
要想压缩成log想到二分,则需要单调数组
联想到LIS优化,想办法转化为最长上升序列问题:
由于a,b数组都是1~n的全排列 ,开出map记录映射情况
记录每个元素在a数组中的位置,将b的每个元素改成它在a中的位置
b数组中的升序序列就是a数组中的升序序列
则求出b的最长上升子序列就是b中含有的a的最长上升序列 
这段序列就是最长公共子序列 

解释:b数组中有一些数 
这些数在原本字符串b中本就从前往后
现在它为升序,说明这些元素在a中也是从前往后
这样就好了,这些数对应的元素在a,b中的顺序一模一样
那这些数对应的元素就是a,b的公共序列,序列长度等于这些升序数的个数
所以最长上升序列的数目就是公共串的长度

举个例子:

A数组有这些数:a b c d e 映射到map里面就是 1 2 3 4 5

B数组有这些数:c e a b d 用map还原回来就可以得到 3 5 1 2 4

这个最长公共序列的答案就是 3 ,也就是序列 a b d 的长度,这样就可以套LIS了

这样就很好理解了,数字本就在a中顺序排列,能在b组里面升序就说明这些在a中的顺序排列也可以在b中顺序排列,这些数又是它本身,就变成公共序列了

#include<bits/stdc++.h>
#define N 100005
typedef long long ll;
using namespace std;
ll b[N],dp[N],f[N];
ll n;
map<ll,ll>M;
ll fun(ll k){
	ll l=0,r=N-1;
	while(l<r-1){
		ll mid=(l+r)/2;
		if(f[mid]<=k){
			l=mid;
		}else{
			r=mid;
		}
	}
	if(f[r]<=k){
		return r;
	}else{
		return l;
	}
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		ll x;
		cin>>x;
		M[x]=i;
	}//map记录每一个数在A中的位置 
	for(int i=1;i<=n;i++){
		ll x;
		cin>>x;
		b[i]=M[x];
	}
	for(int i=1;i<N;i++){
		f[i]=2*N;
	} 
	for(int i=1;i<=n;i++){
		dp[i]=fun(b[i])+1;
		f[dp[i]]=min(b[i],f[dp[i]]);
	}
	ll mm=0;
	for(int i=1;i<=n;i++){
		mm=max(mm,dp[i]);
	}
	cout<<mm;
	return 0;
} 

注:只能求最长公共串的长度,不能求出串本身
如果想要知道这个串得从f数组倒推:
设串长为5,则原串为 f[1] 到 f[5] 的依次连接 
因为他们是每一段的结尾

  • 22
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值