prow2012-A3-0024
从 Trac 迁移的文章
这是从旧校内 Wiki 迁移的文章,可能存在一些样式问题,您可以向 memset0 反馈。
原文章内容如下:
好题。用到了三次单调队列。
题意:一线排列的积木,每个积木有一定的高度,每个可以沿一个方向推倒,会引起连锁反应,问最少碰倒多少个可以推到全部。
{{{
首先求出往左推最远能碰倒的积木Left[i], 以及往右推最远能碰倒的积木Right[i]. (这步单调队列比较容易实现,不多说)
DP f[i] 表示编号1..i的积木全部推倒花费的最小次数
那么f[i] = min( f[Left[i]-1], .... , f[i-1]) +1
这里,只需要很直观的一个想法,假如j < k && f[j] > f[k],那么j在之后的DP中将永远用不到了
所以,DP到i的时候,从后往前检查队列中的候选j,
如果j在i的可取范围内,直接把j踢出队列,因为DP值只增大1,只有最后一个踢出队列的需要保留,
所以有一个还原操作R++。
DP到i时,f[i-1] +1还有可能更新f[Right[i]],而且只需要更新f[Right[i]]——因为前推的“贡献”次数只可能一次,
假设i-1贡献到中间值,那中间值也顶多贡献到Right[i](中间值也只能倒到Right[i]为止).
正是因为f[i]不仅是DP到i时求出的,我忽略了被前推贡献的情况,所以只在第一轮维护队列时踢是没踢干净的,
需要在第一轮踢完,补上最后的R++后,再根据f[i]的大小再踢一轮。在这里WA了3次。
}}}
{{{
#include <algorithm>
#include <stdio.h>
using namespace std;
#define maxn 101000
long long A[maxn],B[maxn],X[maxn],high[maxn];
bool cmp(int i,int j){
if (A[i]==A[j]) return B[i]<B[j];
else return A[i]<A[j];
}
int Left[maxn],Right[maxn],order[maxn];
int N,M;
int oo = 1000000000;
int f[maxn],Q[maxn];
int main(){
int i,j;
while(scanf("%d",&N)!=EOF){
for (i=0;i<N;i++)
order[i] = i,scanf("%lld%lld",&A[i],&B[i]);
sort(order,order+N,cmp);
order[N] = N;
A[N] = -1;
int M = 1;
for (i=0;i<N;i++)
if (A[order[i]]!=A[order[i+1]])
X[M] = A[order[i]],high[M++] = B[order[i]];
N = M;
int L = 0,R = 0;
Q[R++] = 1;
Left[1] = 1;
for (i=2;i<N;i++)
{
Left[i] = i;
while(L<R&&(X[i]-X[Q[R-1]]<=high[i]))
R--,Left[i] = Left[Q[R]];
Q[R++] = i;
}
L = R = 0;
Q[R++] = N-1;
Right[N-1] = N-1;
for (i=N-2;i>0;i--){
Right[i] = i;
while(L<R&&(X[Q[R-1]]-X[i]<=high[i]))
R--,Right[i] = Right[Q[R]];
Q[R++] = i;
}
// for (i=1;i<N;i++)
// printf("%d :%d %d\n",i,Left[i],Right[i]);
for (i=0;i<N;i++)
f[i] = oo;
f[0] = 0;
L = 0,R = 0;
Q[R++] = 0;
for (i=1;i<N;i++){
int tmp = -1;
while(L<R&&Left[i]-1<=Q[R-1]) //第一次维护队列
R--,tmp = f[Q[R]];
if (tmp==-1){
f[i] = min(f[i],f[i-1]+1);
}else {
R++;
f[i] = min(f[i],tmp+1);
}
while(L<R&&f[Q[R-1]]>=f[i]) R--; //第二次维护队列,这里WA了
f[Right[i]] = min(f[Right[i]],f[i-1]+1);
Q[R++] = i;
}
printf("%d\n",f[N-1]);
}
}
}}}
好题。用到了三次单调队列。
题意:一线排列的积木,每个积木有一定的高度,每个可以沿一个方向推倒,会引起连锁反应,问最少碰倒多少个可以推到全部。
首先求出往左推最远能碰倒的积木Left[i], 以及往右推最远能碰倒的积木Right[i]. (这步单调队列比较容易实现,不多说)
DP f[i] 表示编号1..i的积木全部推倒花费的最小次数
那么f[i] = min( f[Left[i]-1], .... , f[i-1]) +1
这里,只需要很直观的一个想法,假如j < k && f[j] > f[k],那么j在之后的DP中将永远用不到了
所以,DP到i的时候,从后往前检查队列中的候选j,
如果j在i的可取范围内,直接把j踢出队列,因为DP值只增大1,只有最后一个踢出队列的需要保留,
所以有一个还原操作R++。
DP到i时,f[i-1] +1还有可能更新f[Right[i]],而且只需要更新f[Right[i]]——因为前推的“贡献”次数只可能一次,
假设i-1贡献到中间值,那中间值也顶多贡献到Right[i](中间值也只能倒到Right[i]为止).
正是因为f[i]不仅是DP到i时求出的,我忽略了被前推贡献的情况,所以只在第一轮维护队列时踢是没踢干净的,
需要在第一轮踢完,补上最后的R++后,再根据f[i]的大小再踢一轮。在这里WA了3次。
#include <algorithm>
#include <stdio.h>
using namespace std;
#define maxn 101000
long long A[maxn],B[maxn],X[maxn],high[maxn];
bool cmp(int i,int j){
if (A[i]==A[j]) return B[i]<B[j];
else return A[i]<A[j];
}
int Left[maxn],Right[maxn],order[maxn];
int N,M;
int oo = 1000000000;
int f[maxn],Q[maxn];
int main(){
int i,j;
while(scanf("%d",&N)!=EOF){
for (i=0;i<N;i++)
order[i] = i,scanf("%lld%lld",&A[i],&B[i]);
sort(order,order+N,cmp);
order[N] = N;
A[N] = -1;
int M = 1;
for (i=0;i<N;i++)
if (A[order[i]]!=A[order[i+1]])
X[M] = A[order[i]],high[M++] = B[order[i]];
N = M;
int L = 0,R = 0;
Q[R++] = 1;
Left[1] = 1;
for (i=2;i<N;i++)
{
Left[i] = i;
while(L<R&&(X[i]-X[Q[R-1]]<=high[i]))
R--,Left[i] = Left[Q[R]];
Q[R++] = i;
}
L = R = 0;
Q[R++] = N-1;
Right[N-1] = N-1;
for (i=N-2;i>0;i--){
Right[i] = i;
while(L<R&&(X[Q[R-1]]-X[i]<=high[i]))
R--,Right[i] = Right[Q[R]];
Q[R++] = i;
}
// for (i=1;i<N;i++)
// printf("%d :%d %d\n",i,Left[i],Right[i]);
for (i=0;i<N;i++)
f[i] = oo;
f[0] = 0;
L = 0,R = 0;
Q[R++] = 0;
for (i=1;i<N;i++){
int tmp = -1;
while(L<R&&Left[i]-1<=Q[R-1]) //第一次维护队列
R--,tmp = f[Q[R]];
if (tmp==-1){
f[i] = min(f[i],f[i-1]+1);
}else {
R++;
f[i] = min(f[i],tmp+1);
}
while(L<R&&f[Q[R-1]]>=f[i]) R--; //第二次维护队列,这里WA了
f[Right[i]] = min(f[Right[i]],f[i-1]+1);
Q[R++] = i;
}
printf("%d\n",f[N-1]);
}
}