萌萌題:一道結合了觀察性質的線性 dp。
觀察
我們先考慮極端情況:所有數相同,所有數降序排列兩種情況。
對於所有數相同的情況,我們發現,最終可以合併出來的區間,最多隻有 \(n \log n\) 個。
怎麼證明?考慮固定右端點,那麼我們想要合併出一個點,就得選 \(2^k\) 個數出來,這就有 \(\log n\) 次選擇方式。總的來說就是有 \(n \log n\) 種選數方式了。
所有數降序排列是多少?我們只需要將最後兩個元素合併,然後繼續往前面合併即可。總體來說有 \(n\) 個。
dp 設計
這題還有個很重要的觀察:對於固定了右端點的時候,假設我們要讓這個數增加 \(v\),那麼合併的方案(區間)一定是唯一的。這就啟發了我們可以設計一個 dp,定義 \(dp_{i,j}\) 表示讓元素 \(i\) 增加 \(j\) 後,合併到的區間的左端點的前一個數是哪裡。
為什麼要前一個數?其實你不要這前一個數其實也可以做。只不過我這樣設計來講比較好寫轉移的程式碼。
接下來就是很顯然的轉移,假設目前合併到的區間的左端點的前一個數為 \(now\),要增加 \(v\):
然後記錄一下合法狀態就好了。
這樣寫對嗎?實際上假了。這是我賽時的做法,當時誤以為一個數最多增量只可能是 \(\log n\)。實際上降序排列就能把增量卡到 \(n\)。
那麼開大小為 \(n\) 的增量可不可以?直接開肯定不行,會 MLE,但是動態開不就行了?根據前面的觀察,我們最多隻有 \(n \log n\) 個合法區間,那麼每個數的增量也最多隻有 \(n \log n\) 個,我們用 vector 模擬動態開 dp 陣列的過程,這道題就 AC 了。
注意,當某個區間無法繼續合併下去的時候,要立刻 break,才能保證複雜度正確,否則會退化為平方級別。
時間複雜度為 \(O(n\log n)\)。
程式碼
#include <bits/stdc++.h>
#define fi first
#define se second
#define lc (p<<1)
#define rc ((p<<1)|1)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pi;
int n,a[100005],ans=0;
vector<int>dp[100005];
int main()
{
freopen("cute.in","r",stdin);
freopen("cute.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)
{
dp[i].emplace_back(i-1);
for(int j=1;j<=100000;j++)
{
int now=dp[i][j-1];
int ta=a[now];
int dt=a[i]+j-1-ta;
if(dt>=0&&dt<dp[now].size())dp[i].emplace_back(dp[now][dt]);
else break;
}
}
for(int i=1;i<=n;i++)ans=max(ans,a[i]+int(dp[i].size())-1);
cout<<ans<<endl;
return 0;
}