P2501 [HAOI2006]数字序列
题目描述
现在我们有一个长度为n的整数序列A。但是它太不好看了,于是我们希望把它变成一个单调严格上升的序列。但是不希望改变过多的数,也不希望改变的幅度太大。
输入输出格式
输入格式:
第一行包含一个数n,接下来n个整数按顺序描述每一项的键值。
输出格式:
第一行一个整数表示最少需要改变多少个数。
第二行一个整数,表示在改变的数最少的情况下,每个数改变的绝对值之和的最小值。
说明
90%的数据n<=6000。
100%的数据n<=35000。
保证所有数列是随机的。
“数据随机”==乱搞 啊哈
陷入了笛卡尔树的坑里
看了题解,大家一致认为第一问灰常简单,第二问灰常毒瘤
我:
好吧,第一问其实有思想的,发现直接求要改变的死活不好弄,不妨使用补集转换的思想,求最多不改变的数字
设(dp_i)代表以(i)为末尾的数字不改变时的最大不改变数字
转移有:
(dp_i=max_{a_i-a_j ge i-j} dp_j +1)
复杂度是(O(N^2))的
我们发现,其实我们是在最大化转移次数
如果把转移条件移项(a_i-i ge a_j-j)
设(b_i=a_i-i),问题就转换成了求(LIS),可以(O(nlogn))求解
第二问 有点微妙 实质上是一个跑不满的(O(N^3))做法,上界极其宽松(当然要写的好才行)
把(a)变得单调上升,等价与把(b)变的单调不降,花费是等价的
设(f_i)为把前(i)项合法的最小花费
转移有:
(f_i=min_{dp_i==dp_j+1} f_j+cost_{i,j})
先不考虑如何计算花费,考虑卡枚举前一维的常数
很显然前(i)项是要跑满的,从哪里转移我们建一个链表就表示转移集合
考虑如何计算费用
发现如果可以转移,那所有的在(b_i)和(b_i)之间的(b)没有值是夹在它们中间的。
它们一定会往两端进行靠拢,可以证明(没看懂原证明),存在一个(k),使(b_i)~(b_k)都为(b_i),使(b_k+1)~(b_j)都为(b_j)
所有我们只需要枚举中间的这个(k)就行啦
代码细节还是很多的,没给值域还是很坑的
Code:
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
#define ll long long
ll min(ll x,ll y){return x<y?x:y;}
ll abs(ll x){return x>0?x:-x;}
const ll N=35002;
ll n,a[N],b[N],g[N],k;
std::vector <ll > dx[N];ll dp[N],s1[N],s2[N];
int main()
{
scanf("%lld",&n);dp[0]=-0x3f3f3f3f,b[n+1]=-dp[0],b[0]=dp[0];
for(ll i=1;i<=n;i++)
scanf("%lld",a+i),b[i]=a[i]-i;
for(ll i=1;i<=n;i++)
{
if(b[i]>=dp[k]) dp[++k]=b[i],g[i]=k;
else
{
g[i]=std::upper_bound(dp+1,dp+1+k,b[i])-dp;
dp[g[i]]=b[i];
}
}
printf("%lld
",n-k);g[++n]=k+1;
for(ll i=0;i<=n;i++) dx[g[i]].push_back(i);
memset(dp,0x3f,sizeof(dp));
dp[0]=0;
for(ll i=1;i<=n;i++)
{
for(ll j=0;dx[g[i]-1][j]<i&&j<dx[g[i]-1].size();j++)
{
ll to=dx[g[i]-1][j];
if(b[i]<b[to]) continue;
for(ll l=to;l<=i;l++) s1[l]=abs((ll)(b[l]-b[to])),s2[l]=abs((ll)(b[l]-b[i]));
for(ll l=to+1;l<=i;l++) s1[l]+=s1[l-1],s2[l]+=s2[l-1];
for(ll l=to;l<=i;l++)
dp[i]=min(s1[l]-s1[to]+s2[i]-s2[l]+dp[to],dp[i]);
}
}
printf("%lld
",dp[n]);
return 0;
}