LuoGu P1020 导弹拦截【O(nlog n)求解LCS问题&一个段最少分为多少下降序列】

在这里插入图片描述

贪心+二分

允许我啰嗦一下写这篇题解的原因(这里嫌烦可以跳过 看一下嘛QAQ):

由于我太弱在看题解中dalao们的思路和代码时没有懂

虽然可能有同思路的 但是我当时看的时候没看懂

所以想写一篇通俗易懂的题解帮助像我一样看不懂dalao题解的同学理解一下

我是借鉴了一篇个人感觉很易懂的博客+直接应用了Dilworth定理才A了的

没有Dilworth定理的证明是因为我不会证 想知道的直接问度娘好了

(有人跟我说前提要会cdq分治 可是我太弱并不会 有空去看看)

这是那篇博客:

https://blog.csdn.net/qq_35078631/article/details/61915067

其实第二问博客中是用一种贪心的方式做的 大家也可以参考一下

下面我会尽量通俗且详细地讲一下二分查找优化的思路
代码也会加上步骤解释 如果有什么错误或者建议可以直接@我

主要变量介绍(也会在代码中注释):

a 数组存所有的导弹 x数组存当前拦截的导弹的高度

len 记录当前最多能拦截多少导弹(实际是最长不升子序列即x数组的长度) 同时是最后的答案

下面是过程模拟环节:

样例:389 207 155 300 299 170 158 65

外层从 1 到 n 枚举 i 扫一遍序列

i = 1 时: x 数组为空 len = 0 所以直接将a [ i ]放进x数组 len + +

(x 数组:389)

i = 2 时: a [ i ] <= x [ len ] 也就是说这一发导弹不高于当前拦截的最后一发导弹的高度 符合条件 直接放进x数组 len + +

(x 数组:389 207)

i = 3 时: 同 i = 2情况

(x 数组:389 207 155)

i = 4 时:a [ i ] > x [ len ] 妈耶高于了 那就拦截不了了 我们考虑这个数还有什么用:假如用300代替207的话 不会破坏子序列的性质 而且后面拦截的导弹还有更大的选择空间 这个在后面就有所体现了

(x 数组:387 300 155)

i = 5 时:我们发现仍然是a [ i ] > x [ len ] 那我们再用299代替155

(x 数组:387 300 299)

i = 6~8 时:都满足a [ i ] <= x [ len ] 太好了 都可以继续拦截

(x 数组:387 300 299 170 158 65)

模拟完毕 在最后一步这种做法的优势( ? ? ? )就显示出来了 如果仍然是387 300 155这个序列的话 就只能再拦截最后一发导弹了 什么?你问我二分在哪?二分查找当然是用来找替换的位置的啦

不知道您会不会有这样的疑问:

如果只是389 207 155 300这个序列的话389 300 155这个顺序是无法拦截的呀(不符合导弹发送顺序)蒟蒻的我对此疑惑

后来想通:

这是不影响最长不升子序列的长度的(也就是389 300 155和389 207 155是一样的) 因为用300代替207只在由于这个改变而引起的后面的x数组更新时(389 207 155——>387 300 299时) 会起到扩大拦截导弹的选择空间的作用 没起到怎么办? 那就假装没变不就完了(这就是389 300 155和389 207 155一样的原因)什么垃圾解释

dilworth定理的通俗讲解

度娘定义:在数学理论中的序理论与组合数学中,Dilworth定理根据序列划分的最小数量的链描述了任何有限偏序集的宽度。
其名称取自数学家Robert P. …
Dilworth定理说明,存在一个反链A与一个将序列划分为链族P的划分,使得划分中链的数量等于集合A的基数。

在这道题中,我们需要将链 a [ 1... n ] a[1...n] a[1...n]分解为最小数量的满足 a [ i ] > = a [ i + 1 ] a[i]>=a[i+1] a[i]>=a[i+1]的有限偏序集,那么它等于反链的基数,即 a [ 1... n ] a[1...n] a[1...n]得最长上升子序列得长度。

#define inf 0x3f3f3f3f
#define ll long long
#define vec vector<int>
//#define P pair<int,int>
#define MAX 100005

int N, a[MAX], dp[MAX], cnt, n = 0, len = 0;
vec v[MAX];

int main() {
	fill(dp, dp + MAX, -inf);
	while (cin >> a[n])a[n] *= -1, n++;

	//a[i]按元素取反,然后求最长下降子序列
	for (int i = 0; i < n; i++) {
		if (a[i] >= dp[len]) dp[++len] = a[i];
		else {
			int id = upper_bound(dp, dp + len, a[i]) - dp;
			dp[id] = a[i];
		}
	}
	cout << len << endl;

	//求正常情况下的最长上升子序列,即得到可以将该序列最多分解的下降子序列的数目
	for (int i = 0; i < n; i++)a[i] *= -1;
	memset(dp, 0, sizeof(dp)); len = 0;
	for (int i = 0; i < n; i++) {
		if (a[i] > dp[len])dp[++len] = a[i];
		else {
			int id = lower_bound(dp, dp + len, a[i]) - dp;
			dp[id] = a[i];
		}
	}
	cout << len << endl;
}

线段树

为何选择线段树?

我们先来看看 O ( n 2 ) O(n^2) O(n2)得算法如何操作的:

for(int i=1;i<=n;i++)
{
	dp[i]=0;
    for(int j=1;j<i;j++)
    {
    	if (a[j]>=a[i]) dp[i]=max(dp[i],dp[j]+1);
    }
}

整个程序段显然分成了两个部分:

  • 遍历DP数组
  • 查找在前面的最大值

很显然,遍历DP数组是不可能舍去的,那么想让效率达到 O ( n l o g n ) O(n logn) O(nlogn)就必须在查找最大值上做手脚。

而解决问题的方案很多。这里我们选择从数据结构上来入手。

如何操作线段树?

线段树的编写之类不多说。这里只需要一棵支持单点修改,区间查询的线段树而已,难度不大。

这里思维难度较大的地方在于——线段树里保存的是最长不下降子序列最大值,那如果最大值所在处的导弹高度低于当前高度呢?

这里又有一手骚操作——把** 当前导弹的高度当做线段树的下标( 注意!不是当前导弹的序号)**

这样,在查询时就只需要查询当前导弹高度至最高导弹高度间的区间最大值即可

所以状态转移方程即为

dp[i]=query(a[i],max_high,1)+1

第二问就不多讲了,只需将最长不上升子序列变成求最长上升子序列即可。原因不再赘述。(Dilworth定理)

每轮循环查询一次,更新一次。一共循环n轮,复杂度堪堪达到 O ( n l o g n ) O(nlogn) O(nlogn)

#include<iostream>
#include<cmath>
#include<cstdlib>
#include<algorithm>
using namespace std;
struct node
{
    int val,l,r;
}dp[50007*4];
int n,max_high,ans;
int a[100007];
void push_up(int p)
{
    dp[p].val=max(dp[p*2].val,dp[p*2+1].val);
}
void build(int l,int r,int p)
{
    if (l==r)
    {
        dp[p].l=dp[p].r=l;
        dp[p].val=0;
        return;
    }
    dp[p].l=l;dp[p].r=r;
    int mid=(l+r)/2;
    build(l,mid,p*2);
    build(mid+1,r,p*2+1);
    push_up(p);
}
void updata(int L,int C,int p)
{
    if (dp[p].l==dp[p].r)
    {
        dp[p].val=C;
        return;
    }
    int mid=(dp[p].l+dp[p].r)/2;
    if (L<=mid) updata(L,C,p*2);
    if (L>mid) updata(L,C,p*2+1);
    push_up(p);
}
int query(int l,int r,int p)
{
    if (l==dp[p].l&&r==dp[p].r)
    {
        return dp[p].val;
    }
    int mid=(dp[p].l+dp[p].r)/2;
    int res=0;
    if (r<=mid) return query(l,r,p*2);
    if (l>mid) return query(l,r,p*2+1);
    return max(query(l,mid,p*2),query(mid+1,r,p*2+1));
}
int main()
{
    n=0;
    while(!cin.eof())
    {
        cin>>a[++n];
        max_high=max(max_high,a[n]);
    }
    build(1,max_high,1);
    for(int i=1;i<=n;i++)
    {
        int tmp=query(a[i],max_high,1)+1;
        updata(a[i],tmp,1);
        if(tmp>ans) ans=tmp;
    }
    cout<<ans<<endl;
    ans=0;
    build(1,max_high,1);
    for(int i=1;i<=n;i++)
    {
        int tmp=query(1,a[i]-1,1)+1;//由于这里是最长上升子序列而非最长不下降子序列,因此a[i]一定要减一!
        updata(a[i],tmp,1);
        if (tmp>ans) ans=tmp;
    }
    cout<<ans;
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值