題目鏈接:https://codeforces.ml/contest/1367/problem/F2
題目大意:
給出一段序列,每次操作可以將一個數移到序列後方或者前方,問最少需要幾步使得序列單調不降。
題目思路:
網上思路看不懂,只能瞎推dp
單看F1,沒有重複數字的情況下
只需要找一段最長的連續上升子序列即可(經典題)了
F2,加入重複數字就不可以這麼考慮了
比如樣例:0 1 0 2 就可以選擇0 1 2 然後把0移動到前面
比如樣例:1 1 1 2 2 3 3 3 2 就選擇1 1 1 2 2 2使得3 3 3 靠後
這樣一看,對於每個數字來說,可以取滿也可以不取滿
用dp[i][0]表示當前數字出現了多少次
用dp[i][1]表示當前數字取滿的的答案
用pre[id]與dp[i][0]同理,不過怕暈而已
首先肯定性質1:
如果一個數字不取滿,它只能出現在末尾或者開頭
1.考慮如果第i個數字不取滿,以第i個數字結束:
(1)i-1個數字取滿
(2)i-1個數字不取滿(此時i-1只能取出現的次數)
解釋一下爲什麼dp[i][0]的狀態特別:
單方面考慮dp[i][0]:dp[i][0]可以繼承dp[i-1][1],dp[i-1][0],但是都是有侷限的,如果繼承了dp[i-1]][1]的狀態,那麼接下來dp[i+1][0]就不能繼承dp[i][0]了(性質1),所以dp[i][0]只是單方面的表示第i個數字目前爲止出現了多少次。
那怎麼統計未取滿結束的答案?
爲了防止以上情況,我們可以另開一個數組sc(用來輔助更新ans):
單看這個方程可能有點暈,把更新ans的方程也一列:
此時就是更新當前id未取滿的最優答案。
sc[id]:
sc[id]的意思就是記錄之前dp[id-1][0],dp[id-1][1]取滿的最大值(都是合法的,不懂可以再看一下不取滿的倆狀態)
但是僅僅記錄這個不夠,因爲還不知道sc[id]記錄的位置到當前位置有多少個id。
我們更新sc[id]的時候更新爲sc[id]-pre[id](減去之前出現過的),這樣到達新位置時,只需要sc[id]+pre[id]+1。
當然主席樹也可以解決上述問題,不過小題大作了
這樣就可以保證性質1的正確的前提下進行轉移。
2.考慮第i個數字取滿
那麼非常簡單了,與不取滿同理
(1)i-1個數字取滿
(2)i-1個數字不取滿(此時i-1只能取出現的次數)
然後同理,設first爲第一次出現數字i時的最大值
然後當pre[id]等於這個數字的次數vis[id]時,則有:
Code:
/*** keep hungry and calm CoolGuang!***/
#include <bits/stdc++.h>
#pragma GCC optimize(3)
//#pragma GCC optimize("Ofast","unroll-loops","omit-frame-pointer","inline")
#include<stdio.h>
#include<queue>
#include<algorithm>
#include<string.h>
#include<iostream>
#define debug(x) cout<<#x<<":"<<x<<endl;
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const ll INF=2e18;
const int maxn=1e6+6;
const int mod=1e9+6;
const double eps=1e-15;
inline bool read(ll &num)
{char in;bool IsN=false;
in=getchar();if(in==EOF) return false;while(in!='-'&&(in<'0'||in>'9')) in=getchar();if(in=='-'){ IsN=true;num=0;}else num=in-'0';while(in=getchar(),in>='0'&&in<='9'){num*=10,num+=in-'0';}if(IsN) num=-num;return true;}
ll n,m,p;
ll num[maxn];
vector<ll>v;
int vis[maxn];
int getid(ll x){
return lower_bound(v.begin(),v.end(),x)-v.begin()+1;
}
ll dp[maxn][2];
ll pre[maxn],first[maxn],sc[maxn];
int main()
{
int T;scanf("%d",&T);
while(T--){
read(n);
v.clear();
for(int i=1;i<=n;i++){
vis[i] = 0;
dp[i][0] = dp[i][1] = 0;
pre[i] = 0;
first[i] = 0;sc[i] = -(1e9+7);
}
for(int i=1;i<=n;i++){
read(num[i]);
v.push_back(num[i]);
}
ll maxl = 0;
sort(v.begin(),v.end());
v.erase((unique(v.begin(),v.end())),v.end());
for(int i=1;i<=n;i++) vis[getid(num[i])]++;
for(int i=1;i<=n;i++){
int id = getid(num[i]);
pre[id]++;
dp[id][0] = dp[id][0] + 1;
if(pre[id] == 1) first[id] = max(dp[id-1][0],dp[id-1][1]);
if(pre[id] == vis[id]) dp[id][1] = first[id] + vis[id];
sc[id] = max(sc[id],max(dp[id-1][0]-pre[id],dp[id-1][1]-pre[id]));
maxl = max(maxl,sc[id]+pre[id]+1);///10
maxl = max(maxl,max(dp[id][0],dp[id][1]));
/// printf("%lld %lld\n",pre[id],sc[id]);
/// printf("%d :%lld %lld\n",id,dp[id][0],dp[id][1]);
}
printf("%lld\n",n-maxl);
}
return 0;
}
/**
1 2
1 2 3 4
4 3 2 1
**/
總結:
這個方法可以抽象:當進行dp時,前面狀態被限制不可以得到正常轉移時,可以新開數組單方面記錄可以轉移的狀態