题目大意:给出一段长度为n(1<=n<=1e6)的序列,把该线段切割几次后重新拼接成一个单调不下降序列,问最少切割几次。
虽然还不是很明白,但是还是记录一下自己的理解吧。
首先做一部分预处理,把所有值做一次离散化,这个是显然需要做的; 把一段连续的相同的值合并为一个值,因为从中间切割的方案可以转化为在这段值左右边界处切割方案,而不会产生更高的代价
然后第一个想法就是贪心。如果把所有不同的值先全都切割开,比如对于预处理过后的序列{1,2,3,1,2},如果切割成{1|2|3|1|2}这样的形式,就显然可以重新拼接成{1|1|2|2|3},但是这显然不是最优解。因为如果切割成{1|2 3|1 2}这样的形式的话,排列得到{1 | 1 2 | 2 3 }这样子也可以得到同样的序列,而切割次数仅为2。
那么容易得到一个贪心的目标:尽量将两个相邻的已切割的片段合并为一个片段,则 。而直接的贪心策略就是寻找在原序列里位置相邻,且前一段的值为后一段的值减一的两段,这样满足条件的连续两段的集合的大小就是总的最多可以合并的次数。
但是这样会遇到一个问题,如果序列中有 1 | 2 | 3 这样的片段,将如何分割呢,是合并成1 2| 3,还是1 | 2 3呢,前一种选择会在{1 2 | 3 | 2 3}这样的情况下满足能重新组合得到{1 2 | 2 3 | 3}这样一个单调不下降序列的条件,而后一种在此情况下无法满足条件;相对的,后者会在{1| 2 3 |1 2}这样的情况下满足能重新组合得到{1 | 1 2 | 2 3}这样一个单调不下降序列的条件,而后一种在此情况下无法满足条件;
这就表明如果要合并某相邻两个片段,则需要依据后续的操作来做出判断,贪心解法就行不通了。
贪心不行就说明记录的信息不够,就再考虑考虑DP。DP需要考虑什么状态呢,依据前面的贪心的判断条件,合并两个相邻的离散化后的数值段
,则需要满足
而且此合并操作不能和前面已经选择过的合并操作冲突;
什么时候会冲突呢,对于合并
和
,如果这四段在原序列中的位置都不相同,则显然不会冲突;
但是如果既合并了
,又合并了
呢?
这会导致如果在序列中有另一段值为
的数值段,则就会导致其无法插入正确的位置。
所以,合并
和
会发生冲突的充要条件就是
那么为了保证一个连接处不会被计入两次,需要DP的第一维来确认当前是合并了值为多少的两个数值段,这是因为合并得到两个相同的{1 2},就无法重新组合得到满足条件的序列;而为了保证不会发生冲突,需要DP的第二维来记住最后一次合并是合并了哪个位置的两个线段,有了这两个维度就可以得到最多能合并多少个相邻的线段。
而相对应的转移方程就是
于是可以得到一下的代码:
#include <cmath>
#include <queue>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <bitset>
#include <map>
#include <set>
#define MAXN 401000
#define MAXM 901000
using namespace std;
inline void read(int &x){
char ch;
bool flag=false;
for (ch=getchar();!isdigit(ch);ch=getchar())if (ch=='-') flag=true;
for (x=0;isdigit(ch);x=x*10+ch-'0',ch=getchar());
x=flag?-x:x;
}
inline void write(int x){
static const int maxlen=100;
static char s[maxlen];
if (x<0) { putchar('-'); x=-x;}
if(!x){ putchar('0'); return; }
int len=0; for(;x;x/=10) s[len++]=x % 10+'0';
for(int i=len-1;i>=0;--i) putchar(s[i]);
}
int n;
int a[ MAXN ];
int tmp[ MAXN ];
int num[ MAXN ];
int f[1010][1010];
map<int,int> M;
vector<int> pos[MAXN];
int main(){
read(n);
for (int i=1;i<=n;i++)
read(a[i]);
for (int i=1;i<=n;i++)
tmp[i]=a[i];
sort(tmp+1,tmp+n+1);
int cnt=0;
tmp[0]=-1;
for (int i=1;i<=n;i++)
if ( tmp[i]!=tmp[i-1] )
M[tmp[i]]=++cnt;
int tot=0;
a[0]=-1;
for (int i=1;i<=n;i++)
if ( a[i]!=a[i-1] )
tmp[++tot]=M[a[i]];
for (int i=1;i<=tot;i++)
a[i]=tmp[i];
for (int i=1;i<=tot;i++)
pos[a[i]].push_back(i);
int pre=0;
for (int i=0;i<pos[1].size();i++)
if ( a[pos[1][i]+1] == 2 )
{
f[1][pos[1][i]]=1;
pre=1;
}
for (int i=2;i<cnt;i++)
{
for (int j=0;j<pos[i].size();j++)
{
int x=pos[i][j];
if ( a[ x+1 ]==i+1 )
{
f[i][x]=pre;
for (int k=0;k<pos[i-1].size();k++)
{
int y=pos[i-1][k];
if ( ( y+1 != x ) || ( pos[i].size()==1 ) )
f[i][x]=max(f[i][x],f[i-1][y]+1);
else
f[i][x]=max(f[i][x],f[i-1][y]);
}
}
}
for (int j=0;j<pos[i].size();j++)
pre=max(f[i][ pos[i][j] ] ,pre );
}
printf("%d\n",tot-1-pre);
return 0;
}
显然这样的空间是O(n^2)的,还不能AC此题
因为每次更新只和上一层相关,可以利用滚动数组优化一下使得空间变为O(n),但是时间复杂度最坏还是会达到O(n^2),会TLE
仔细思考转移的过程,对于当前
的转移,只需要考虑上一层
时取得
的最大值和次大值时,
值和最后一次合并的位置,就可以转移得到这一层
时取得的
的最大值和次大值时,
值和最后一次合并的位置。而最后的
的最大值就是全局下的最多合并次数。
为什么只需要记录最大值和次大值就可以了呢,因为如果枚举的当前的合并操作和取得上一层的最大值的合并操作序列冲突,则必然不会和取得上一层的次大值的合并操作序列,而不需要考虑最大值和次大值以外的
值。
最后AC的代码如下:
#include <cmath>
#include <queue>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <bitset>
#include <map>
#include <set>
#define MAXN 1001000
using namespace std;
inline void read(int &x){
char ch;
bool flag=false;
for (ch=getchar();!isdigit(ch);ch=getchar())if (ch=='-') flag=true;
for (x=0;isdigit(ch);x=x*10+ch-'0',ch=getchar());
x=flag?-x:x;
}
inline void write(int x){
static const int maxlen=100;
static char s[maxlen];
if (x<0) { putchar('-'); x=-x;}
if(!x){ putchar('0'); return; }
int len=0; for(;x;x/=10) s[len++]=x % 10+'0';
for(int i=len-1;i>=0;--i) putchar(s[i]);
}
int n;
int a[ MAXN ];
int tmp[ MAXN ];
int num[ MAXN ];
map<int,int> M;
vector<int> pos[MAXN];
pair<int,int> MAXX[2],_MAXX[2];
int main(){
read(n);
for (int i=1;i<=n;i++)
read(a[i]);
for (int i=1;i<=n;i++)
tmp[i]=a[i];
sort(tmp+1,tmp+n+1);
int cnt=0;
tmp[0]=-1;
for (int i=1;i<=n;i++)
if ( tmp[i]!=tmp[i-1] )
M[tmp[i]]=++cnt;
int tot=0;
a[0]=-1;
for (int i=1;i<=n;i++)
if ( a[i]!=a[i-1] )
tmp[++tot]=M[a[i]];
for (int i=1;i<=tot;i++)
a[i]=tmp[i];
for (int i=1;i<=tot;i++)
pos[a[i]].push_back(i);
int pre=0,now=1;
for (int i=0;i<pos[1].size();i++)
if ( a[pos[1][i]+1] == 2 )
{
_MAXX[now].first=1;
_MAXX[now].second=pos[1][i];
if (_MAXX[now] > MAXX[now] )
swap(_MAXX[now],MAXX[now]);
}
swap(now,pre);
for (int i=2;i<cnt;i++)
{
_MAXX[now]=_MAXX[pre];
MAXX[now]=MAXX[pre];
for (int j=0;j<pos[i].size();j++)
{
int x=pos[i][j];
if ( a[x+1] != a[x]+1 )
continue;
if ( ( MAXX[pre].second+1 != x )|| ( pos[i].size()==1 ) )
_MAXX[now]=max(_MAXX[now],make_pair( MAXX[pre].first+1,x ) );
else
_MAXX[now]=max(_MAXX[now],make_pair( _MAXX[pre].first+1,x ) );
if (_MAXX[now] > MAXX[now] )
swap(_MAXX[now],MAXX[now]);
}
swap(now,pre);
}
printf("%d\n",tot-1-MAXX[pre].first);
return 0;
}