【基于贪心的动态规划】【NOI2006】千年虫

时间:2022-09-08 15:17:00
千年虫是远古时代的生物,时隔几千万年,千年虫早已从地球上销声匿迹,人们对其知之甚少。考古生物学家最近开始对其有了兴趣,因为一批珍贵的千年虫化石被发现,这些化石保留了千年虫近乎完整的形态。理论科学家们根据这些化石归纳出了千年虫的一般形态特征模型,并且据此判定出千年虫就是蜈蚣的祖先!但科学家J发现了实际与理论的一些出入,他仔细的研究了上百个千年虫化石,发现其中大部分千年虫的形态都不完全符合理论模型,这到底是什么因素造成的呢?理论科学家K敏锐的指出,千年虫的形态保存在化石中很有可能发生各种变化,即便最细微的变化也能导致它不符合模型。于是,摆在科学家面前的新问题诞生了:判断一个化石中的千年虫与理论模型的差距有多大?具体来说,就是根据一个千年虫化石的形态A,找到一个符合理论模型的形态B,使 得B是最有可能在形成化石时变成形态A。 
【基于贪心的动态规划】【NOI2006】千年虫
理论学家提出的“千年虫形态特征模型”如下(如上图所示):躯体由头、尾、躯干、足四大部分构成。 1.头,尾用一对平行线段表示。称平行于头、尾的方向为x方向;垂直于x的方向为y方向; 2.在头尾之间有两条互不相交的折线段相连,他们与头、尾两条线段一起围成的区域称为躯干,两条折线段都满足以下条件:拐角均为钝角或者平角,且包含奇数条线段,从上往下数的奇数条垂直于x方向。 3.每条折线段从上往下数的第偶数条线段的躯干的另一侧长出一条足,即一个上、下底平行于x方向的梯形或矩形,且其中远离躯干一侧的边垂直于x方向。注意:足不能退化成三角形(即底边的长度均大于零),躯干两侧足的数目可以不一样。(如下图,左边有4条足,右边有5条足) 
【基于贪心的动态规划】【NOI2006】千年虫
可见,x-y直角坐标系内,躯干和所有足组成的实心区域的边界均平行或垂直于坐标轴。为了方便,我们假设所有这些边界的长度均为正整数。因此可以认为每个千年虫的躯体都由一些单位方格拼成。每个单位方格都由坐标(x,y)唯一确定。设头尾之间的距离为n,则我们可以用2×n个整数来描述一条千年虫B(如右图):将B沿平行x轴方向剖分成n条宽度为1的横条,每个横条最左边一格的x坐标设为Li,最右一格的的x坐标设为Ri。则(n,L1,L2,..,Ln,R1,R2,..Rn)就确定了一条千年虫。由于岁月的侵蚀,在实际发现的化石中,千年虫的形状并不满足上面理论模型的规则,一些格子中的躯体已经被某些矿物质溶解腐蚀了。地质、物理、生物学家共同研究得出: 1、腐蚀是以格子为单位的,只能一整格被腐蚀; 2、腐蚀是分步进行的,每一步只有一格被腐蚀; 3、如果去掉一个格子后躯体不连通了,那么这个格子当前不会被腐蚀; 4、如果一个格子的左边邻格和右边邻格都还没被腐蚀,那么这个格子当前不会被腐蚀; 5、与头相邻的格子不能全部被腐蚀,与尾相邻的格子不能全部被腐蚀;倘若满足上面五条,我们仍然可以用(n,L’1,L’2,..,L’n,R’1,R’2,..R’n)来描述一个化石里头的千年虫的形态。其中L’i≤R’i。例如下图: 
【基于贪心的动态规划】【NOI2006】千年虫
现在你的任务是,输入一个化石里的千年虫的描述A,找一个满足理论模型的千年虫的描述B,使得B可以通过腐蚀过程得以变为A,且由B转化为A的代价(须被腐蚀的格子数)最少。输出此最小代价。Input第一行为一个整数n;以下n行,每行两个整数,其中第i行为两个整数L’i,R’i,用一个空格分开;保证输入数据合法。Output仅一行,为一个整数,表示最少代价。Sample Input74 43 43 51 32 22 43 3Sample Output3HINT
【基于贪心的动态规划】【NOI2006】千年虫
30%的数据 n≤100, 0≤L’i≤R’i≤10050%的数据 n≤1000, 0≤L’i≤R’i≤100070%的数据 n≤100000, 0≤L’i≤R’i≤1000100%的数据 n≤1000000, 0≤L’i≤R’i≤1000000
此题十分考察选手的语文素养……

题目大意是求出最少加多少格子使得图形的两边变成“梳子”形。

那么显然可以看出左右两边相对独立,于是可以单独计算。
设第i行的初始高度为a[i],最终高度为b[i],f[i][j][s]表示前i行中当前行最终高度为j,凹凸性为s(0凹1凸)所需要加的最少块数。
则有:
f[i][j][0] = min{f[i - 1][k][1], f[i - 1][j][0]} + j - a[i] (k > j)
f[i][j][1] = min{f[i - 1][k][0], f[i - 1][j][1]} + j - a[i] (k < j)
代码:

/***************************\
* @prob: NOI2006 worm *
* @auth: Wang Junji *
* @stat: TLE: 30 *
* @date: June. 1st, 2012 *
* @memo: 动态规划 *
\***************************/
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <string>

const int maxN = 500010, INF = 0x3f3f3f3f;
int f[2][maxN][2], L[maxN], R[maxN], Now[maxN], n, Lim;

inline int calc()
{
int pst = 0, ths = 1; memset(f[ths], 0x3f, sizeof f[ths]); ++Lim;
for (int j = Now[1]; j < Lim + 1; ++j) f[ths][j][0] = j - Now[1];
for (int i = 2; i < n + 1; ++i)
{
pst ^= 1, ths ^= 1; memset(f[ths], 0x3f, sizeof f[ths]);
for (int j = Now[i]; j < Lim + 1; ++j)
{
int &f0 = f[ths][j][0], &f1 = f[ths][j][1];
f0 = f[pst][j][0], f1 = f[pst][j][1];
for (int k = Now[i - 1]; k < j; ++k) f1 = std::min(f1, f[pst][k][0]);
for (int k = Lim; k > j; --k) f0 = std::min(f0, f[pst][k][1]);
f0 += j - Now[i], f1 += j - Now[i];
}
}
int Min = INF;
for (int j = Now[n]; j < Lim + 1; ++j) Min = std::min(Min, f[ths][j][0]);
return Min;
}

int main()
{
freopen("worm.in", "r", stdin);
freopen("worm.out", "w", stdout);
scanf("%d", &n); int max_L = 0, min_R = INF;
for (int i = 1; i < n + 1; ++i)
scanf("%d%d", L + i, R + i),
max_L = std::max(max_L, L[i]),
min_R = std::min(min_R, R[i]);
Lim = 0;
for (int i = 1; i < n + 1; ++i) Lim = std::max(Lim, Now[i] = R[i] - min_R);
int ans = calc(); Lim = 0;
for (int i = 1; i < n + 1; ++i) Lim = std::max(Lim, Now[i] = max_L - L[i]);
printf("%d\n", ans += calc()); return 0;
}

直接朴素计算显然要超时。

先加一个小小的优化。
用单调队列的思想可以将方程的转移优化到O(1)(实际上不需要队列,利用单调性就可以了)。

/***************************\
* @prob: NOI2006 worm *
* @auth: Wang Junji *
* @stat: TLE: 40 *
* @date: June. 1st, 2012 *
* memo: 动态规划 *
\***************************/
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <string>

const int maxN = 60010, INF = 0x3f3f3f3f;
int f[2][maxN][2], L[maxN], R[maxN], Now[maxN], n, Lim;

inline int calc()
{
int pst = 0, ths = 1; memset(f[ths], 0x3f, sizeof f[ths]); ++Lim;
for (int j = Now[1]; j < Lim + 1; ++j) f[ths][j][0] = j - Now[1];
for (int i = 2; i < n + 1; ++i)
{
pst ^= 1, ths ^= 1; int best0 = INF, best1 = INF;
for (int j = 0; j < Now[i]; ++j) f[ths][j][0] = f[ths][j][1] = INF;
for (int j = Now[i - 1]; j < Now[i]; ++j)
best0 = std::min(best0, f[pst][j][0]);
for (int j = Now[i]; j < Lim + 1; ++j)
f[ths][j][1] = std::min(f[pst][j][1], best0) + j - Now[i],
j >= Now[i - 1] ? (best0 = std::min(best0, f[pst][j][0])) : 0;
for (int j = Lim; j > Now[i] - 1; --j)
f[ths][j][0] = std::min(f[pst][j][0], best1) + j - Now[i],
best1 = std::min(best1, f[pst][j][1]);
}
int Min = INF;
for (int j = Now[n]; j < Lim + 1; ++j) Min = std::min(Min, f[ths][j][0]);
return Min;
}

int main()
{
freopen("worm.in", "r", stdin);
freopen("worm.out", "w", stdout);
scanf("%d", &n); int max_L = 0, min_R = INF;
for (int i = 1; i < n + 1; ++i)
scanf("%d%d", L + i, R + i),
max_L = std::max(max_L, L[i]),
min_R = std::min(min_R, R[i]);
Lim = 0;
for (int i = 1; i < n + 1; ++i) Lim = std::max(Lim, Now[i] = R[i] - min_R);
int ans = calc(); Lim = 0;
for (int i = 1; i < n + 1; ++i) Lim = std::max(Lim, Now[i] = max_L - L[i]);
printf("%d\n", ans += calc()); return 0;
}

然后就看题解说第i行的最终高度的取值范围是
[a[k], a[k] + 3) (|k - i| < 3).(将这几个区间并起来)
(由于能力有限,无法对此进行证明。)
那么,有了这个结论,就可以优化到线性复杂度了。
代码:

/******************************\
* @prob: NOI2006 worm *
* @auth: Wang Junji *
* @stat: Accepted. *
* @date: June. 2nd, 2012 *
* @memo: 基于贪心的动态规划 *
\******************************/
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <string>
using std::min; using std::max;

const int maxN = 100010, INF = 0x3f3f3f3f;
struct Node
{
int cnt, pos[20]; int &operator[](int Ind) {return pos[Ind];}
} node[2]; int f[2][20][2], L[maxN], R[maxN], Now[maxN], n;

inline int getint()
{
int res = 0; char tmp;
while (!isdigit(tmp = getchar()));
do res = (res << 3) + (res << 1) + tmp - '0';
while (isdigit(tmp = getchar()));
return res;
}

inline void update(int &a, int b) {if (b < a) a = b; return;}

inline static int calc()
{
int pst = 0, ths = 1; node[ths].cnt = 0;
for (int j = 0; j < 3; ++j)
for (int k = Now[j]; k < Now[j] + 3; ++k)
if (k >= Now[0]) node[ths][node[ths].cnt++] = k;
//得到需要被计算的几个区间。
for (int j = 0; j < node[ths].cnt; ++j)
f[ths][j][0] = node[ths][j] - Now[0], f[ths][j][1] = INF;
for (int i = 1; i < n; ++i)
{
ths ^= 1, pst ^= 1; node[ths].cnt = 0;
for (int j = max(i - 2, 0); j < i + 3 && j < n; ++j)
for (int k = Now[j]; k < Now[j] + 3; ++k)
if (k >= Now[i]) node[ths][node[ths].cnt++] = k;
for (int j = 0; j < node[ths].cnt; ++j)
{
f[ths][j][0] = f[ths][j][1] = INF;
for (int k = 0; k < node[pst].cnt; ++k)
{
if (node[pst][k] > node[ths][j]) update(f[ths][j][0], f[pst][k][1]);
else if (node[pst][k] < node[ths][j]) update(f[ths][j][1], f[pst][k][0]);
else update(f[ths][j][0], f[pst][k][0]), update(f[ths][j][1], f[pst][k][1]);
}
f[ths][j][0] += node[ths][j] - Now[i], f[ths][j][1] += node[ths][j] - Now[i];
}
}
int Min = INF;
for (int j = 0; j < node[ths].cnt; ++j) update(Min, f[ths][j][0]);
return Min;
}

int main()
{
freopen("worm.in", "r", stdin);
freopen("worm.out", "w", stdout);
n = getint();
for (int i = 0; i < n; ++i) L[i] = getint(), Now[i] = getint();
int ans = calc();
for (int i = 0; i < n; ++i) Now[i] = maxN - L[i];
printf("%d\n", ans += calc()); return 0;
}