最长不降子序列 n log n 方案输出与 Dilworth 定理 - 动态规划模板

朴素算法

不必多说,\(O(n^2)\) 的暴力 dp 转移。

优化算法

时间为 \(O(n \log n)\) ,本质是贪心,不是 dp 。

思路是维护一个单调栈(手写版),使这个栈单调不降。

  • 当该元素 \(\ge\) 栈顶元素时,把这个元素压入栈中。
  • 否则,在单调栈中找到第一个大于该元素的项,把这一项改为这个元素。(因为要使每个元素尽可能小,才有更大的拓展空间)。这个过程可以用二分实现,推荐使用码量更小的 upper_bound 或 lower_bound 。

最后,单调栈里元素个数即为最长不降子序列的长度。

#include <bits/stdc++.h>
using namespace std;
int n,a[100005],sn[100005],tp=0;
int main()
{
	sn[0]=-0x3f3f3f3f;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		if(a[i]>=sn[tp])
		{
			sn[++tp]=a[i];
		}
		else
		{
			int tmp=upper_bound(sn+1,sn+tp+1,a[i])-sn;
			sn[tmp]=a[i];
		}
	}
	cout<<tp<<endl;
	return 0;
}

输出方案

这样的贪心看似会破坏最长不降子序列的结构,不能输出;实际上我们可以通过记录一条链的方式输出正确的方案。

我们可以同时记录下单调栈里每个元素在原序列中的下标。

  • 对于直接压入栈顶的元素,我们把它在链表中的前驱设为它压入之前的栈顶元素。
  • 对于二分修改的元素,我们把它在链表中的前驱设为目前单调栈中它所处的位置的前一个元素。(不能设为被修改元素的前驱,因为这个前驱可能不是修改之后的。)

最后从栈顶的元素开始,遍历其前驱,记录在 vector 中,最后倒序输出即可。

#include <bits/stdc++.h>
using namespace std;
int n,a[100005],pre[100005],si[100005],sn[100005],tp=0;
vector<int>v;
int main()
{
	sn[0]=-0x3f3f3f3f;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		if(a[i]>=sn[tp])
		{
			pre[i]=si[tp];
			si[++tp]=i;
			sn[tp]=a[i];
		}
		else
		{
			int tmp=upper_bound(sn+1,sn+tp+1,a[i])-sn;
			pre[i]=si[tmp-1];
			si[tmp]=i;
			sn[tmp]=a[i];
		}
	}
	cout<<tp<<endl;
	int now=si[tp];
	while(now)
	{
		v.push_back(a[now]);
		now=pre[now];
	}
	reverse(v.begin(),v.end());
	for(auto x:v)cout<<x<<' ';
	return 0;
}

Dilworth 定理

挺难理解的,目前只能死记,或者通过贪心理解。

例题:导弹拦截第二问

Dilworth 定理:对于任何有限偏序集,其最大反链中元素的数目等于最小链覆盖中链的数目。

在导弹拦截中,“最小链覆盖” 就相当于是求最少要用多少导弹系统,而 “最大反链” 就相当于是本问的做法:**求最长上升子序列。(因为最小链指最长不升子序列,那么反链即为最长上升子序列)

导弹拦截-不记录方案版

#include <bits/stdc++.h>
using namespace std;
int n=1,h[100005],sn[100005],tp=0,un[100005];
bool cmp(const int &val,const int &e)
{
	return e<val;
}
int main()
{
	while(cin>>h[n])n++;
	n--;
	sn[0]=0x3f3f3f3f;
	for(int i=1;i<=n;i++)
	{
		if(sn[tp]>=h[i])
		{
			sn[++tp]=h[i];
		}
		else
		{
			int tmp=upper_bound(sn+1,sn+tp+1,h[i],cmp)-sn;
			sn[tmp]=h[i];
		}
	}
	cout<<tp<<endl;
	tp=0;
	for(int i=1;i<=n;i++)
	{
		if(un[tp]<h[i])
		{
			un[++tp]=h[i];
		}
		else
		{
			int tmp=lower_bound(un+1,un+tp+1,h[i])-un;
			un[tmp]=h[i];
		}
	}
	cout<<tp<<endl;
	return 0;
}

导弹拦截-记录方案版

#include <bits/stdc++.h>
using namespace std;
int n=1,h[100005],si[100005],sn[100005],tp=0,ui[100005],un[100005],pre[100005];
vector<int>v;
bool cmp(const int &val,const int &e)
{
	return e<val;
}
int main()
{
	while(cin>>h[n])n++;
	n--;
	sn[0]=0x3f3f3f3f;
	for(int i=1;i<=n;i++)
	{
		if(sn[tp]>=h[i])
		{
			pre[i]=si[tp];
			sn[++tp]=h[i];
			si[tp]=i;
		}
		else
		{
			int tmp=upper_bound(sn+1,sn+tp+1,h[i],cmp)-sn;
			pre[i]=si[tmp-1];
			sn[tmp]=h[i];
			si[tmp]=i;
		}
	}
	cout<<tp<<endl;
	int now=si[tp];
	while(now)
	{
		v.push_back(h[now]);
		now=pre[now];
	}
	reverse(v.begin(),v.end());
	for(auto x:v)cout<<x<<' ';
	cout<<endl;
	tp=0;
	for(int i=1;i<=n;i++)
	{
		if(un[tp]<h[i])
		{
			un[++tp]=h[i];
			ui[tp]=i;
		}
		else
		{
			int tmp=lower_bound(un+1,un+tp+1,h[i])-un;
			un[tmp]=h[i];
			ui[tmp]=i;
		}
	}
	cout<<tp<<endl;
	return 0;
}

以第 \(i\) 个数为结尾的最长上升子序列

例题:合唱队形

直接把二分时求到的要修改的数的下标便是以它为结尾的最长上升子序列的长度。

因为它后面的是比他高的,前面是比他矮的,并且满足以它为结尾。

#include <bits/stdc++.h>
using namespace std;
int n,h[105],up[105],down[105],ans=0,sn[105],tp=0;
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)cin>>h[i];
	sn[0]=-0x3f3f3f3f;
	for(int i=1;i<=n;i++)
	{
		int tmp=lower_bound(sn+1,sn+tp+1,h[i])-sn;
		up[i]=tmp;
		if(tmp>tp)sn[++tp]=h[i];
		else sn[tmp]=h[i];
	}
	tp=0;
	sn[0]=-0x3f3f3f3f;
	for(int i=n;i>=1;i--)
	{
		int tmp=lower_bound(sn+1,sn+tp+1,h[i])-sn;
		down[i]=tmp;
		if(tmp>tp)sn[++tp]=h[i];
		else sn[tmp]=h[i];
	}
	for(int i=1;i<=n;i++)
	{
		ans=max(ans,up[i]+down[i]-1);
	}
	cout<<n-ans;
	return 0;
}
posted @ 2024-07-22 12:33  KS_Fszha  阅读(66)  评论(0编辑  收藏  举报