准备不到一个月后的保研机试,最近开始重新拾起一些算法题练手。记得高中竞赛那会就最喜欢动规题了,也是我个人最喜欢和最擅长的算法,没想到5年过去了居然还是没怎么变做起来很有兴趣也很顺手。我认为动规的核心在于“状态设计”,基本上拿到问题,只要设计出了合理的状态,那么转移也就不难写(因为状态设计是合理的),然后问题也就解决了大半。对于一些较复杂的动规问题,合理的状态设计有难度,但也很有“美感”。
这道题的大意是有一个2*B(1=<B<=15000000)的大矩阵,该矩阵中有N(N<=1000)头奶牛(即大矩阵中有指定的N个点)。现在想用K个小矩阵去覆盖这全部N个点,求在保证N个点被完全覆盖的情况下K个小矩阵的面积的最小值。
这道题有一点小难度,最起码比那些一眼就能看穿的动规水题更有质量一些。
B很大,而N很小,离散化的处理方式是很自然的。然后用动规来做。具体的思路都写在代码注释里了。这里就直接贴代码了。
// Poj 2430 Lazy Cows
// By Victor Li
// 2016-08-28
#include <stdio.h>
#include <stdlib.h>
#include <algorithm>
#include <string.h>
using namespace std;
struct coordinate{
int x;
int label;
int sum;
};
bool coordinateCompare(const coordinate & a, const coordinate & b){
return ( a.x<b.x || (a.x==b.x && a.label<b.label) );
}
int f[1001][1001][4];
int min_(int x,int y){
if (x<y) return x;
else return y;
}
int main(){
int N, K, B;
scanf("%d%d%d",&N,&K,&B);
//读入奶牛序列信息并排序(列号为第一关键字,行号为第二关键字)
coordinate * Co = (coordinate *)calloc(N,sizeof(coordinate));
for (int i=0;i<N;i++) scanf("%d%d",&Co[i].label,&Co[i].x);
sort(Co,Co+N,coordinateCompare);
coordinate * MerCo = (coordinate *)calloc(N,sizeof(coordinate));
int i = 0 , N_ = 0;
//排序后,将序列中处在同一列上的两头奶牛合并(Merge)为一个点,方便后面DP计算过程
while (i<N){
if (i<N-1 && Co[i].x == Co[i+1].x){
MerCo[N_].x = Co[i].x;
//如果一列上有两头奶牛,label设为0
MerCo[N_].label = 0;
if (N_ == 0) MerCo[N_].sum = 2;
else MerCo[N_].sum = MerCo[N_-1].sum + 2;
N_++;
i+=2;
} else {
MerCo[N_].x = Co[i].x;
//一列上只有一头奶牛,label设为它所在的行号(1或2)
MerCo[N_].label = Co[i].label;
if (N_ == 0) MerCo[N_].sum = 1;
else MerCo[N_].sum = MerCo[N_-1].sum + 1;
N_++;
i+=1;
}
}
free(Co);
/*
状态设计简单解释:
f[i][j]表示用j个矩阵覆盖前i列的所有奶牛,覆盖面积最小的方案
但是,如果我们仅将状态设计f[i][j],却发现很难写出状态转移方程
因为这样的状态设计不能满足“无后效性”的原则
多加一维k,k=0..3,分别对应当前状态最后一列的4种不同的形态
f[i][j][0]:
-----
| 最后一列处是一个 len*2的矩阵的末尾(len是多少不重要,不用记录)
|
-----
f[i][j][1]:
-----
| 最后一列的第一行是一个len*1的矩阵的末尾,第二行处为空,无矩阵覆盖
-----
-----
f[i][j][2]: 与f[i][j][1]类似,是第一行无覆盖,第二行有覆盖
f[i][j][3]:
-----
| 最后一列处为两个宽度为1的矩阵的末尾
-----
|
-----
一开始做时,我没设计k=3的情况,导致Wrong Answer
没有k=3的情况下,一个典型的错例:
3 2 100
1 1
2 50
1 100
这个例子的最优方案就是两个宽度为1的矩阵,如果没有k=3,原来的状态设计和转移方案无法表达这种方案
*/
//动态规划边界初值
memset(f,127,sizeof(f));
if (MerCo[0].label == 0) {
f[0][1][0] = 2;
if (K>1) f[0][2][3] = 2;
} else{
f[0][1][0] = 2;
f[0][1][ MerCo[0].label ] = 1;
if (K>1) f[0][2][3] = 2;
}
//转移计算:i,j大循环,循环体内依次计算f[i][j][0] ~ f[i][j][3]
//结合各个状态的实际含义,仔细地考察各种转化情况的可能性,不要有遗漏即可
//以下转移过程结合代码即可理解其具体含义,这里就不多做解释了
for (i=1;i<N_;i++)
for (int j=1;j<=min_(K,MerCo[i].sum);j++){
f[i][j][0] = min_(f[i][j][0], f[i-1][j][0] + (MerCo[i].x-MerCo[i-1].x)*2 );
for (int k=0;k<4;k++)
f[i][j][0] = min_(f[i][j][0] , f[i-1][j-1][k]+2);
if (MerCo[i].label > 0) {
int l = MerCo[i].label;
f[i][j][l] = min_(f[i][j][l], f[i-1][j][l] + (MerCo[i].x-MerCo[i-1].x) );
f[i][j][l] = min_(f[i][j][l], f[i-1][j][3] + (MerCo[i].x-MerCo[i-1].x) );
for (int k=0;k<4;k++)
f[i][j][l] = min_(f[i][j][l] , f[i-1][j-1][k]+1);
}
f[i][j][3] = min_(f[i][j][3], f[i-1][j][3] + (MerCo[i].x-MerCo[i-1].x)*2 );
if (MerCo[i].label == 0 && j>1) {
for (int k=0;k<4;k++)
f[i][j][3] = min_(f[i][j][3], f[i-1][j-2][k]+ 2 );
}
for (int k=1;k<4;k++){
f[i][j][3] = min_(f[i][j][3], f[i-1][j-1][k] + (MerCo[i].x-MerCo[i-1].x) + 1 );
}
}
free(MerCo);
//答案为f[N_-1][K][0..3]中的最小值
if (f[N_-1][K][0]<f[N_-1][K][1] && f[N_-1][K][0]<f[N_-1][K][2] && f[N_-1][K][0]<f[N_-1][K][3] ) printf("%d\n",f[N_-1][K][0]);
else if (f[N_-1][K][1]<f[N_-1][K][2] && f[N_-1][K][1]<f[N_-1][K][3]) printf("%d\n",f[N_-1][K][1]);
else if (f[N_-1][K][2]<f[N_-1][K][3]) printf("%d\n",f[N_-1][K][2]);
else printf("%d\n",f[N_-1][K][3]);
return 0;
}