题目链接: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时,前面状态被限制不可以得到正常转移时,可以新开数组单方面记录可以转移的状态