Making the Grade(POJ3666)
题目大意:
给出长度为n的整数数列,每次可以将一个数加1或者减1,最少要多少次可以将其变成单调增或者单调减(不严格).
题解:
1.一开始我有一个猜想,就是不管怎么改变,最终的所有数都是原来的某个数。然而我并不会证明,然而我属于那种不彻底弄清楚就不会去写的那种顽固分子,于是就拖了好几天。网络上有很多关于此题的题解,确实用了这个猜想来离散化,但是都是讲怎么dp,然后最后扯一句“由于数据比较大,可以离散化”之类的话,要么就是相当粗略的证明(也许已经说的够清楚了只不过我没理解...)。
2.今天早上起来洗漱的时候,感觉头脑比较清醒,再次想了一下这个问题,想到一个自认为正确的证明:
记原来的数从小到大排序后分别是$a_1\ a_2\ a_3\cdots a_n$ 修改后从左到右分别是$b_1\ b_2\ b_3\cdots b_n$. 为了方便描述,在数轴上标出这些点,称为关键点。
假设存在$a_s<b_i<=b_{i+1}<=\cdots <=b_j<a_{s+1}$
情况一:如果这些b都相等,那么把这些b都改成$a_s$或者$a_{s+1}$ 肯定会有一种更优。
情况二:如果不全相等,那么肯定存在 $b_p\ b_{p+1}\ b_{p+2}\cdots b_q$,他们的值相等,那么把他们移到左边的关键点或者右边的关键点,肯定有一种会更加优. 不断这样移动,最后就变成情况一了。
综上至少存在一种最优方案,最终的所有数都是原来的某个数。
因此可以离散化之后做dp,dp[i][j]表示把前i个数变成单调增(不严格)且第i个数变成原来第j大的数的最小代价。
dp[i][j]=min{dp[i-1][1...j]}+abs(a[i]-b[j]).
单调减(不严格)的情况也一样,更加方便的是可以把原数组倒转后做单调增的dp。
另外这题和CF Codeforces Round #371 (Div. 1)的C题类似,有个超简单的nlogn做法。可以看这篇:http://www.cnblogs.com/vb4896/p/5894578.html
代码:
1 #include <iostream> 2 #include <cstdio> 3 #include <cmath> 4 #include <cstring> 5 #include <algorithm> 6 #include <vector> 7 #include <map> 8 #include <cstdlib> 9 #include <set> 10 using namespace std; 11 12 #define X first 13 #define Y second 14 #define Mod 1000000007 15 #define N 3010 16 typedef long long ll; 17 typedef pair<int,int> pii; 18 19 inline int Mul(int x,int y){return 1ll*x*y%Mod;} 20 inline int Add(int x,int y){return ((x+y)%Mod+Mod)%Mod;} 21 22 int n,m; 23 int a[N],b[N]; 24 ll dp[N][N],Ans=1ll<<60; 25 26 void Solve() 27 { 28 for (int j=1;j<=m;j++) dp[1][j]=abs(b[j]-a[1]); 29 for (int i=2;i<=n;i++) 30 { 31 ll tmp=1ll<<60; 32 for (int j=1;j<=m;j++) 33 { 34 tmp=min(tmp,dp[i-1][j]); 35 dp[i][j]=abs(b[j]-a[i]); 36 dp[i][j]+=tmp; 37 38 } 39 } 40 for (int j=1;j<=m;j++) Ans=min(Ans,dp[n][j]); 41 } 42 43 int main() 44 { 45 //freopen("in.in","r",stdin); 46 //freopen("out.out","w",stdout); 47 48 scanf("%d",&n); 49 for (int i=1;i<=n;i++) scanf("%d",&a[i]),b[i]=a[i]=a[i]-i; 50 sort(b+1,b+n+1); 51 for (int i=1;i<=n;i++) if (i==1 || b[i]!=b[i-1]) b[++m]=b[i]; 52 53 Solve(); 54 printf("%I64d\n",Ans); 55 56 return 0; 57 }