CF713C Sonya and Problem Wihtout a Legend


前言

\(\text{O}(n^2)\) DP方法。

这一道题的重点在于离散化,网上有七八成的题解都讲的不是很清晰,所以我在这里讲一下。

upd - 2021.8.8 : 修正笔误。

upd - 2020.7.31 :原来的证明有一点问题,合并后的函数应该是满足严格递增的,已修改。

upd - 2021.5.21 :修改了图床并更正原来的一处笔误,所以重新提交一次。

upd - 2021.5.22 : 修正以前错误的排版,添加了更多的说明。

思路:

既然这是线性 DP。

从以下两个角度出发:

  1. 我从哪里来(当前状态可以由哪些状态得到)?

  2. 我到哪里去(当前状态可以推出哪几个状态)?

我们不难想到:

\(f[i][j]\) 表示 \(a[i]\) 变成 \(re[j]\) 所需的最小操作次数。

\(a[i]\)\(re[j]\) 是哪里来的啊?

我们设原序列为 \(a[]\),我们想要达到的“理想”序列为 \(re[]\)

那么,代码如下:

    for(register long long i=1;i<=n;++i){
		cin>>a[i];
		a[i]-=i;
		re[i]=a[i];
	}
	sort(re+1,re+n+1);

接下来我就讲讲为什么要这样做:

为什么是 re[i]=a[i]-i 呢?

很好理解,我在这里举个例子。

(这里举的例子是 POJ3666,因为离散的方法和此题一样,所以我就以 POJ3666 作为例子讲解这题的离散化)。

先不管这道题的要求。我们先假设加一减一的操作变为了修改操作(POJ3666)。

那么先来一组数据:

\(a[]:\{1,3,2,3,5\} , (n=5)\)

如果我们想要修改它,使得它是单调不减(这里也是 POJ3666 )的。

要怎么修改?

显然,我们的第一思路是这么修改:

\(a[]:\{1,2,3,4,5\}\)

因为这里有重复的3。

所以,我们需要更多的次数去修改它。

为了避免这种情况,我们给每个 \(a[i]\) 减去 \(i\)

为什么呢?

因为 \(i\) 在这里一定是严格递增的(你从头往后扫,下标难道不递增?),现在看一下减去 \(i\) 之后是怎么样的:

\(re[]:\{ 0,1,-1,-1,0\}\)

现在还是有重复的,不过先不要着急。

我画一个图。

这是你的 \(re\) 序列(不下降):

cSK6C4.png

这是你的 \(i\) (所有的 \(i\) 组成的序列)

cSKc8J.png

那我们将这两个序列合并,这个合并的序列一定是这样的:

cSKc8J.png

其实就是一个不等式的加减。

举个例子:

\(re[]=\{ 1,2,3,3,3\}\)

\(i[]=\{1,2,3,4,5\}\)

合并之后:

\(2,4,6,7,8\),是不是满足严格递增?

好的,这里可能会有一点绕。

我们再重新看一看:

\(re[]:\{ 0,1,-1,-1,0\}\)

现在我们只需要使这一个 \(re\) 中不下降的元素尽量多,那么我们需要修改的次数就会尽量的少(最优)。

(也就是要求一个最长不下降子序列再减去元素个数)。

好了,把这一个离散的思路放到我们这一题,只需要改一下条件就可以啦。

(没听懂自行返回再来几遍,我讲的已经比较清晰了)。

现在离散化已经搞定了。那么是时候看方程了。

根据状态的定义,\(f[i][j]\) 只可能从 \(f[i-1][j]\) 转移过来。

那么我们转移过后(也就是修改一次)的代价就是 \(|a[i]-re[j]|\)

(这里是加一减一,所以代价是它们的差的绝对值)。

然后为了最优,我们记录一下这一层之前的“最优”,拿来和现在的“决策”比较一下就行(代码里的 minv)。

方程:

\[\begin{cases}minv=\min(minv,f[i-1][j])\\f[i][j]=minv+|a[i]-re[j]|\end{cases} \]

(最开始的时候 \(minv\) 要赋值为一个超级大的值)。

很好,到这里这道题就基本完成了,如果没有听懂的同学请从头再多看几遍。

AC记录

下面给出代码实现(数据大所以用long long):

#include 
using namespace std;

long long f[3001][3001];//f[i][j]表示a[i]变为re[j]时的最小操作次数 
long long n;
long long a[3001];//原序列 
long long re[3001];//目标序列 
long long minn,res=2e18;//这里要赋很大的值,否则以CF的毒瘤数据你会WA。

long long abs_ll(long long a){
	return a>=0?a:-a;
}

namespace Accepted{
	void zjk_ak_ioi(){
		cout<>n;
	for(register long long i=1;i<=n;++i){
		cin>>a[i];
		a[i]-=i;
		re[i]=a[i];//离散化 
	}
	sort(re+1,re+n+1);//排一遍,使目标序列严格递增 
	for(register long long i=1;i<=n;++i){
		f[1][i]=abs_ll(a[1]-re[i]);//给f一个初值,这里是预处理
	}
	for(register long long i=2;i<=n;++i){
		minn=2e18;//这里每一次都要赋值,否则会错(因为每次操作都要用,也就是每一层都有不同的情况) 
		for(register long long j=1;j<=n;++j){
			minn=min(minn,f[i-1][j]);
			f[i][j]=minn+abs_ll(a[i]-re[j]);//转移
		}
	}
	for(register int i=1;i<=n;++i){
		res=min(res,f[n][i]);//从整个序列的情况去找最小值 
	}
	Accepted::zjk_ak_ioi();//喊出zjkAKIOI,借用神的力量,让我AC! 
}

代码里那个奇怪的函数不用管啦(个人习惯),你只需要直接输出 res 就好了。

一些类似的变式:

  • Poj3666 - Making the Grade

  • LuoguP2501 - [HAOI2006]数字序列

相关