「HAOI 2006」数字序列
description:
求解将 \(n\) 个数字组成的序列变成严格递增子序列的最小改变次数和最小代价,代价则为 \(a_i - k\) , \(k\) 为改后的数字。$n\leq 3.5\times 10^4 , a_i \leq 10^5 $
solution :
Part 1
第一问,求解最小改变次数 :
我们正着想,显然不是那么好想,考虑反过来去想,考虑一下最多更够有多少个被保留不更改,那么我们考虑一下 \(a_i , a_j\)不更改的条件显然是 :
- \(a_i < a_j\)
- 改变 \([i +1 , j- 1]\) 中的数能够使得 \([i ,j]\) 单调递增,那么就有 \(a_i - a_j \leq i - j\) , 移项,就能够得到 \(a_i - i \leq a_j - j\)
那么,我们构造数列 \(b\) , 使得 \(b_i = a_i - i\) , 就求解 \(b\) 的最长不下降子序列, \(n - ret\) 就是答案。
Part 2
显然难度就是在第二问上了。
使 \(a\) 单调递增的代价,和 \(b\) 单调不降的代价是一致的。
显然在 \(b\) 的单调递增的序列中,相邻的两个元素 \(b_l , b_r\) 在对于任意的 \(i \in [l ,r]\) ,都不存在 \(b_l \leq b_i \leq b_r\) 。所以,对于任意 \(i\in [l ,r]\) ,都有 $b_i < b_l \ \ or \ \ b_i > b_r $ , 那么这时候需要考虑一下 \(b_i\) 对于代价的影响,也就是,我们考虑怎么改变 \(b_i\) ,从而使得代价和最小。
有一个结论:
存在一个 \(k \in [l ,r]\),并且在 \(\forall i \in[l , k] , b_i = l , \forall i \in[k + 1 , r],b_i = b_r\) 时,代价和最小。
我们发现,那么大个序列,只考虑一下开头结尾和中间一个值就能够解决吗,显然很突兀。
证明: 先略。
Part 3
设 \(f_i\) 表示最后一位是 \(b_i\) 时,单调不降的最小代价 。
枚举一下 \(j\) , \(j\) 需要满足如下条件 :
- \(j < i ,b_j \leq b_i\)
- \(i,j\) 是上文中提到的,\(b\) 序列中单调不降的相邻的元素。
\(k\)显然不会特别地大,所以暴力枚举 \(k\) ,从而得到 \(f_i\), 状态转移方程为 :
\(w(j ,k)\) 表示将 \([j,i]\) 中 以 \(k\) 为分界点的最小代价,运用上述 \(Part \ \ 2\) 的结论,我们就能够得到, \(f_i\) 的递推式即为
后面的东西可以用前缀和优化一下。
Code
/*
Author : Zmonarch
知识点
*/
#include <bits/stdc++.h>
#define int long long
#define qwq register
#define qaq inline
#define inf 2147483647
const int kmaxn = 1e6 + 10 ;
const int kmod = 998244353 ;
qaq int read(){
int x = 0 , f = 1 ; char ch = getchar() ;
while(!isdigit(ch)) {if(ch == '-') f = - 1 ; ch = getchar() ;}
while( isdigit(ch)) {x = x * 10 + ch - '0' ; ch = getchar() ;}
return x * f ;
}
int n , lth;
int minn[kmaxn] , f[kmaxn] , b[kmaxn];
int g[kmaxn] , pre[kmaxn] , suf[kmaxn] ;
std::vector<int> ed[kmaxn] ;
qaq void Sum(int l , int r , int L , int R) {
pre[l] = suf[r + 1] = 0 ;
for(qwq int i = l + 1 ; i <= r ; i++)
pre[i] = pre[i - 1] + std::abs(b[i] - L) ; // 前缀和
for(qwq int i = r ; i >= l + 1 ; i--)
suf[i] = suf[i + 1] + std::abs(b[i] - R) ; // 后缀和
}
signed main() {
n = read() + 1 ;
for(qwq int i = 1 ; i <= n - 1 ; i++) b[i] = read() - i ;
b[n] = inf , b[0] = - inf ;
minn[1] = b[1] , lth = 1 , g[1] = 1 , ed[1].push_back(1) ;
for(qwq int i = 2 ; i <= n ; i++) // 求解最长不下降子序列。O(nlogn)
{
if(b[i] >= minn[lth]) minn[++lth] = b[i] , g[i] = lth ;
else
{
int x = std::upper_bound(minn + 1 , minn + 1 + lth , b[i]) - minn ;
minn[x] = b[i] ; g[i] = x ;
}
}
for(qwq int i = 0 ; i <= n ; i++) ed[g[i]].push_back(i) ; // 找到先驱
memset(f , 63 , sizeof(f)) ; f[0] = 0 ;
for(qwq int i = 1 ; i <= n ; i++)
{
for(qwq int p = 0 ; p < (int)ed[g[i] - 1].size() ; p++)
{
int j = ed[g[i] - 1][p] ; // 去寻找在单调不下降子序列中相邻的那个地方
if(i < j || b[j] > b[i]) continue ; // 判断合法性。
Sum(j , i , b[j] , b[i]) ; // 利用前缀和优化
for(qwq int k = j ; k < i ;k++)
f[i] = std::min(f[i] , f[j] + pre[k] + suf[k + 1]) ; // 直接状态转移
}
}
printf("%lld\n%lld\n" , n - lth , f[n]) ;
return 0 ;
}