1. 程式人生 > 其它 >dp 求遞增修改最少次數——實數篇(洛谷P3902)

dp 求遞增修改最少次數——實數篇(洛谷P3902)

技術標籤:洛谷演算法

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時,就發生方法上的轉變了,即方式二的操作方式比方式一的更優秀。