NWERC-2017 Problem A. Ascending Photo DP
題目大意:給出一段長度為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;
}