今天我们来拆解一道经典的动态规划结合单调队列优化的题目——PTA-Little Bird。这道题来自POI 2014,考察了对动态规划状态转移的优化能力。我们先从题目本质入手,逐步构建解决方案。
题目描述n棵树排成一排,每棵树有高度d_i。q只小鸟要从第1棵树飞到第n棵树,每只鸟的飞行能力不同:第i只鸟在第j棵树时,可以飞到j+1到j+k_i范围内的树。如果目标树高度≥当前树,劳累值+1。我们需要为每只鸟计算从第1到第n棵树的最小劳累值。
关键观察点:
最直观的解法是动态规划:
但这种解法时间复杂度是O(nk),当k接近n时会退化为O(n²),无法通过1e6的数据规模。
我们需要优化状态转移过程。观察到:
这提示我们可以使用单调队列来维护可能成为最优决策的候选集。具体来说,我们维护一个队列,其中:
我们使用一个双端队列(可以用数组模拟)来维护候选决策点:
cpp复制int q[1000001]; // 单调队列,存储树的下标
int head, tail; // 队列头尾指针
int f[1000001]; // dp数组,f[i]表示到达i的最小劳累值
初始状态:
对于每只鸟(每个k值),我们重新计算f数组:
关键代码段解析:
cpp复制while(head<=tail && i-q[head]>x) head++; // 维护窗口范围
f[i] = (a[q[head]]>a[i]) ? f[q[head]] : f[q[head]]+1; // 状态转移
while(head<=tail && (f[q[tail]]>f[i] || (f[q[tail]]==f[i]&&a[q[tail]]<=a[i])))
tail--; // 维护队列单调性
q[++tail] = i; // 入队
这在题目给定的约束下(n≤1e6,q≤25)是完全可行的。
cpp复制#include <cstdio>
int n,m,x,head,tail,a[1000001],q[1000001],f[1000001];
// 快速读入优化(处理大规模数据必备)
int read(){
char ch=getchar();int res=0,w=1;
while(ch<'0'||ch>'9') {if(ch=='-') w=-1;ch=getchar();}
while(ch>='0'&&ch<='9') {res=res*10+ch-'0';ch=getchar();}
return res*w;
}
int main(){
n=read();
for(register int i=1;i<=n;i++) a[i]=read();
m=read();
while(m--) {
x=read();
head=tail=1; q[tail]=1;
for(register int i=2;i<=n;i++) {
// 维护窗口范围
while(head<=tail && i-q[head]>x) head++;
// 状态转移
if(a[q[head]]>a[i]) f[i]=f[q[head]];
else f[i]=f[q[head]]+1;
// 维护队列单调性
while(head<=tail &&
(f[q[tail]]>f[i] ||
(f[q[tail]]==f[i] && a[q[tail]]<=a[i])))
tail--;
q[++tail]=i;
}
printf("%d\n",f[n]);
}
return 0;
}
快速输入输出:使用getchar()实现的read()函数比scanf更快,对于1e6规模的数据至关重要。
寄存器变量:使用register修饰循环变量,提示编译器将其放入寄存器加速访问。
数组模拟队列:比STL deque更快,避免了动态内存分配的开销。
条件判断优化:在维护队列单调性时,将f值比较放在前面,利用短路求值特性提高效率。
窗口范围错误:
初始条件错误:
单调队列维护条件错误:
小规模测试:
打印中间状态:
cpp复制printf("i=%d: head=%d tail=%d q=[",i,head,tail);
for(int j=head;j<=tail;j++) printf("%d ",q[j]);
printf("] f=%d\n",f[i]);
边界情况测试:
对于n=1e6的数据:
这类算法可用于:
通过这道题,我们学到了:
对于信奥选手的建议:
重要提示:在竞赛中,遇到1e6规模的数据一定要首先考虑O(n)或O(nlogn)算法,暴力DP通常无法通过。
这道题的巧妙之处在于将看似复杂的飞行过程转化为一个可以用单调队列优化的DP问题。在实际编码时,我建议先用注释明确每个步骤的意图,再逐步实现,这样可以减少出错概率。另外,对于信奥比赛中的大数据量问题,养成使用快速输入输出的好习惯非常重要。