dp 求递增修改最少次数——实数篇
洛谷P3902
本题AC代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int s[N];
int main()
{
ios::sync_with_stdio(false);
int n,t;
int j=0;
int sum=0;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>t;
if(t>s[j])
{
s[++j]=t;
}
else
{
*lower_bound(s+1,s+j+1,t)=t;
sum++;
}
}
cout<<sum<<endl;
return 0;
}
虽然这道题是一道普及/提高-难度的题,但我感觉他其中的思想很妙。
本蒟蒻看题解看懵了,洛谷的每一篇题解都没有说为什么可以这样写。
然后看别人的AC代码想了很久,终于想出了一个why。
首先,先说明一下lower_bound()函数的用法。
lower_bound()的作用是找到数组中第一个大于等于t的位置。
划重点:返回值是一个地址。
它的参数构成是:lower_bound(数组开始,数组末尾,需查找的数t)
lower_bound(int* first,int* last,t);
欲做题,先看题:
划重点:数字为实数
在做这道题的时候我没看到这个点,以为要修改后的数一定是整数(虽然真的有这种题,但这里不做扩展),这样想会直接导致难度飙升。
想了很久,感觉做不出,就看了题解,之后就懵逼了,看不懂牛犇们在写什么,为什么牛犇们就用了一个lower_bound()函数就把题给做出来了?
下面就聊聊牛犇的思想之妙。
我们先分析一下什么改变的方式:题目明确说明将数字改为实数,这就意味着你可以把这个数改为无限接近上一个数的一个数。
比如:1 3 4 3 我要使这个数列递增,就可以把3改为3.1或者3.01或者3.000000…1。lim一下就是可以改为3。
再进一步分析,题目要求修改次数最小,那么怎么保证修改次数最少呢?
修改次数和你选择的修改方式有关,这题的修改方式有两种。
若当前输入的数比上一个数小,那么就要进行修改使得这个数列使完全递增的数列。
第一种 直接修改当前数,使得这个数大于上一个数,而又因为可以改为一个实数,那么就意味着可以趋近等于上一个数。那么进一步就可以把这一步视为删去。即直接把这个数删除,这一步的操作次数为1。
第二种 将数 t 之前的所有大于数 t 的数全部修改为小于数 t 的数。他的操作次数>=1。
那么如果从单一时刻来看,选择第一种一定是最优解,但是单一时刻不能表示全局。
举个例子:
1 7 8 9 3 4 5 6
如果每次都用第一种修改方式,把这个数列修改为一个完全递增的数列要操作4 次,但如果把 7 8 9 改为 2 2.1 2.2 就只用操作3 次。
所以现在dp的方向出来了:如何确定操作方式使得全局操作次数一定最小呢?
这就要聊聊牛犇们的思想了。
根据上面聊过的,可以把一个数组抽象为两个部分
即: 比最大小的数+最大的数
如果当前输入的数 t 大于最大的数,就更新最大值,即把 t 压入数组中。
若输入的数比最大的数小,就在比最大的数小的数中用Lower_bound()函数找到它的位置,然后将这个数变为 t 。
看到这里大家肯定懵了,这样为什么可以这样做?不是说好要选操作方式吗?
我们细品,之前提过,操作一实际上是把这个输入的数 t 直接删除,可以理解成对原数组不做改变,而我们的所有问题都集中在怎么选使操作次数最小。
所以这样想,假如我们每次遇到数 t 比 最大的数 小的时候,能修改次数就加一。我们可以用这次操作去进行操作方式一,也可以是用这一次操作去改变 比最大的数小的数中的一个值(局部方式二)。而因为方式一不改变原数组,而方式二改变原数组,所以我们只用对比,是通过操作方式一的操作次数少还是通过操作方式二的操作方式少就行。
举个例子:
1 4 5 6 3 4 5 6
当输入到 3 时 我们改变 4 的值为 3。数组:1 3 5 6
当输入到 4 时 我们改变 5 的值为 4。数组: 1 3 4 6
当输入到 5 时 我们改变 6 的值为 5。数组: 1 3 4 5
当输入到 6 时 直接压入数组 : 1 3 4 5 6
所以最小操作数为 3。不难发现当输入的值为 5时,就发生方法上的转变了,即方式二的操作方式比方式一的更优秀。