题目
背景
H N S D F Z HNSDFZ HNSDFZ的同学们为了庆祝春节,准备排练一场舞
描述
n
n
n个人选出
3
m
3m
3m个人,排成
m
m
m组,每组
3
3
3人。
站的队形为较矮的
2
2
2个人站两侧,最高的站中间。
从对称学角度来欣赏,左右两个人的身高越接近,则这一组的“残疾程度”越低。
计算公式为
h
=
(
a
−
b
)
2
h=(a-b)^2
h=(a−b)2 (
a
,
b
a,b
a,b为较矮的2人的身高)
那么问题来了。
现在候选人有
n
n
n个人,要从他们当中选出
3
m
3m
3m个人排舞蹈,要求总体的“残疾程度”最低。
格式
输入格式
第一行为
m
,
n
m,n
m,n。
第二行
n
n
n个数字,为每个人的身高,保证升序排列。
输出格式
输出最小“残疾程度”。
样例输入输出
输入
9 40
1 8 10 16 19 22 27 33 36 40 47 52 56 61 63 71 72 75 81 81 84 88 96 98 103 110 113 118 124 128 129 134 134 139 148 157 157 160 162 164
输出
23
限制
各个测试点 1 s 1s 1s
提示
m
≤
1000
,
n
≤
5000
m\le1000,n\le5000
m≤1000,n≤5000
数据保证
3
m
≤
n
3m\le n
3m≤n
思路
思路一
这很明显是一道
d
p
dp
dp的题,不难想到
d
p
[
i
,
j
]
dp[i,j]
dp[i,j]表示前
i
i
i个人组成
j
j
j个队伍的状态定义。
暴力枚举最后一个组,考虑的因素很多:最高的肯定是每一组中间的那一个,那么最高的可以直接拿来当高子。可是第二高的呢?他有没有可能跟着最高的组成队伍呢?是个问题。开个
b
o
o
l
bool
bool数组记录?那就变回了
d
f
s
dfs
dfs,想都不用想,
T
L
E
TLE
TLE。
思路二
如果在最优答案中出现了这样的组合:
(
a
,
b
,
e
)
(a,b,e)
(a,b,e)和
(
c
,
d
,
f
)
(
a
,
b
<
e
(c,d,f)(a,b<e
(c,d,f)(a,b<e且
c
,
d
<
f
c,d<f
c,d<f且
a
<
c
<
b
<
d
)
a<c<b<d)
a<c<b<d)则
h
h
h的和为
(
b
−
a
)
2
+
(
d
−
c
)
2
(b-a)^2+(d-c)^2
(b−a)2+(d−c)2
那么将其替换为
(
a
,
c
,
e
)
(a,c,e)
(a,c,e)和
(
b
,
d
,
f
)
(b,d,f)
(b,d,f)
仍有
a
<
c
<
b
<
e
,
b
<
d
<
f
a<c<b<e,b<d<f
a<c<b<e,b<d<f
所以h的和为
(
c
−
a
)
2
+
(
d
−
b
)
2
(c-a)^2+(d-b)^2
(c−a)2+(d−b)2
因为
c
−
a
<
b
−
a
c-a<b-a
c−a<b−a且
d
−
b
<
d
−
c
d-b<d-c
d−b<d−c
所以
h
h
h的和一定变小了
如图所示:黑色肯定没有红色的组合好(别忘了保证升序;延伸向右边的线连着高子)
所以我选完某一个组合之后,两个矮子中间的就不能再当矮子了。
那高子怎么定?转换思路:
d
p
[
i
,
j
]
dp[i,j]
dp[i,j]表示后
i
i
i个人,而非前
i
i
i个人
这样剩下的就自然成为了高子咯!
所以状态转移方程就变成了
d
p
[
i
,
j
]
=
m
i
n
(
d
p
[
k
+
1
,
j
−
1
]
+
(
a
[
k
]
−
a
[
i
]
)
2
,
d
p
[
i
+
1
,
j
]
)
dp[i,j]=min(dp[k+1,j-1]+(a[k]-a[i])^2,dp[i+1,j])
dp[i,j]=min(dp[k+1,j−1]+(a[k]−a[i])2,dp[i+1,j])
辅以一点点小技巧:滚动数组+斜率优化。大佬肯定早就不稀罕用了
实现见代码一。
思路三
这还是有点难啊……就没有一个更简单的方法吗?
当然有!听说下雨天,dp和贪心更配哦
倘若你选了第i个人,并在
i
+
1
i+1
i+1到
n
n
n中寻找他的队友时,你有没有想过,为什么不直接让
i
i
i和
i
+
1
i+1
i+1匹配呢?你要是选了
k
(
k
>
i
+
1
)
k(k>i+1)
k(k>i+1),
i
+
1
i+1
i+1到
k
−
1
k-1
k−1之间没有人当矮子,你为什么不让
i
+
1
i+1
i+1当矮子?
至于高子的问题,你只要让
3
j
≤
i
3j\le i
3j≤i就可以了,这样选完之后自动会剩下几个用来当高子。
(其实此处证明的不够严谨,但是你可以想象到:最前面的
j
−
1
j-1
j−1组必然占用
3
j
−
3
3j-3
3j−3个人,再加上我们现在正在计算的这两个矮子,共有
3
j
−
1
3j-1
3j−1个人会被使用。那么除去这些人,剩下的就是
i
−
3
j
+
1
i-3j+1
i−3j+1个人,只要
3
j
≤
i
3j\le i
3j≤i即
i
−
3
j
+
1
>
0
i-3j+1>0
i−3j+1>0,就一定还有人没有被占用,而
i
i
i和
i
+
1
i+1
i+1是右数i个中最矮的两个,所以剩下的那个人可以当高子)
实现见代码二
代码
代码一
#include <cstdio>
#include <iostream>
using namespace std;
typedef long long LL;
const LL llong_inf=(1ll<<62)-1;
# define For(i,a,b) for(register int i=(a);i<=(b);++i)
# define Rep(i,a,b) for(register int i=(a);i>=(b);--i)
inline int readint(){
int a=0;char c=getchar();bool f=0;
while(c<'0'||c>'9'){if(c=='-')f=1;c=getchar();}
while('0'<=c&&c<='9') a=a*10+c-'0',c=getchar();
if(f) a=-a;return a;
}
inline LL Pow(const LL &x){
return x*x;
}
struct Point{
LL x,y;
Point(LL X=0,LL Y=0):x(X),y(Y){}
};
Point operator-(const Point &a,const Point &b){
return Point(a.x-b.x,a.y-b.y);
}
LL operator*(const Point &a,const Point &b){
return a.x*b.y-b.x*a.y;
}
const int MAXN=5002;
int num[MAXN],q[MAXN],head,tail;
LL dp[MAXN][2];
Point point[MAXN];
void Push(const int &Index){
Point now,last;
while(tail-1>=head)
{
now=point[Index]-point[q[tail]];
last=point[q[tail]]-point[q[tail-1]];
if(now*last>=0)
--tail;
else
break;
}
q[++tail]=Index;
}
void Maintain(const Point &xielv){
Point now;
while(head+1<=tail)
{
now=point[q[head+1]]-point[q[head]];
if(now*xielv>=0)
++head;
else
break;
}
}
int main()
{
int m=readint(),n=readint();
For(i,1,n) num[i]=readint();
dp[n-1][1]=llong_inf;
Rep(i,n-2,1) dp[i][1]=min(dp[i+1][1],Pow(num[i+1]-num[i]));
int now_dp=1;
For(j,2,m)
{
now_dp^=1;
head=1,tail=0;
int origin=n-3*j+1;
point[origin]=Point(2*num[origin],dp[origin+1][now_dp^1]+Pow(num[origin]));
dp[origin][now_dp]=dp[origin+2][now_dp^1]+Pow(num[origin+1]-num[origin]);
Rep(i,origin-1,1)
{
Push(i+1);
Maintain(Point(1,num[i]));
dp[i][now_dp]=dp[q[head]+1][now_dp^1]+Pow(q[head]-num[i]);
dp[i][now_dp]=min(dp[i][now_dp],dp[i+1][now_dp]);
point[i]=Point(2*num[i],dp[i+1][now_dp^1]+Pow(num[i]));
}
}
cout<<dp[1][now_dp];
return 0;
}
代码二
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;
inline int readint(){int x;scanf("%d",&x);return x;}
inline long long sqr(const long long &x){return x*x;}//pow(x,2)
const int MAXN=5000,MAXM=1000;const long long llong_inf=(1ll<<61)-1;
long long dp[MAXN+2][MAXM+2];int a[MAXN+2];
int main()
{
int m=readint(),n=readint();
for(int i=1;i<=n;++i)
{
a[i]=readint();
for(int j=1;j<=m;++j)
dp[i][j]=llong_inf;
}
for(int j=1;j<=m;++j)
for(int i=n-3*j+1;i>=1;--i)
dp[i][j]=min(dp[i][j],min(dp[i+2][j-1]+sqr(a[i+1]-a[i]),dp[i+1][j]));
long long ans=llong_inf;
for(int i=n-3*m+1;i>=1;--i)
ans=min(ans,dp[i][m]);
printf("%lld",ans);
return 0;
}
后记
其实一开始并没有想到简单的思路三,而是真的老老实实地打了一个斜率优化的 d p dp dp……只能说,好的思路造就较少代码量与较低错误率,但学的东西多了以后,就算不是正解思路也能搞出个名堂来!