1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
| \documentclass{article}
\usepackage{graphicx} \usepackage{amsmath} \usepackage{amssymb} \usepackage{xeCJK} \usepackage{multicol} \usepackage{tocloft} \usepackage{xcolor} \usepackage[cachedir=_minted-main]{minted} \usepackage[a4paper, left=2cm, right=2cm, top=2.5cm, bottom=2.5cm]{geometry} \usepackage{tikz} \usetikzlibrary{arrows.meta, positioning}
\setminted{ breaklines=true, breakanywhere=true, fontsize=\small, frame=single, bgcolor=gray!5, tabsize=2, }
\renewcommand{\cftsecleader}{\cftdotfill{\cftdotsep}}
\usepackage[colorlinks=true, linkcolor=black, anchorcolor=black, citecolor=black, filecolor=black, menucolor=black, runcolor=black, urlcolor=blue]{hyperref}
\definecolor{selc}{RGB}{0,160,80} \definecolor{rejc}{RGB}{180,180,180} \definecolor{queryc}{RGB}{50,120,200} \definecolor{jumpc}{RGB}{220,80,60}
\title{1005 列车停放站 题解} \author{Gospel\_rock} \date{}
\begin{document} \maketitle
\section{题意}
给定 $n$ 辆列车的停放区间 $[l_i, r_i]$。$m$ 次询问,每次给出区间 $[x, y]$,求在 $[x, y]$ 内最多能停放多少辆列车。选中的列车区间必须完全包含在 $[x, y]$ 内,且互不重叠(端点可以相切)。
\section{贪心策略}
忽略多次询问,先考虑单次查询 $[x, y]$:从所有满足 $x \le l_i$ 且 $r_i \le y$ 的列车中,选出最多的互不重叠列车。这是经典的\textbf{区间调度最大化}问题,贪心策略为:
\begin{quote} 维护一个指针 $p$(初始 $p = x$)。每次从所有 $l_i \ge p$ 且 $r_i \le y$ 的列车中,选择\textbf{右端点 $r_i$ 最小}的那辆,然后令 $p \leftarrow r_i$,重复此过程直到无法再选。 \end{quote}
\textbf{为什么总是选右端点最小的?} 设有两辆可选列车 $A = [l_A, r_A]$ 和 $B = [l_B, r_B]$,其中 $r_A < r_B$。 选完 $A$ 后,剩余可用空间为 $[r_A, y]$;选完 $B$ 后,剩余空间为 $[r_B, y]$。 因为 $r_A < r_B$,前者严格包含后者,所以任何在 $[r_B, y]$ 中可行的后续方案,在 $[r_A, y]$ 中同样可行,反之不然。因此选 $A$ 一定不比选 $B$ 差。
图~\ref{fig:greedy} 以样例数据(查询 $[1, 4]$)演示了这一过程。
\begin{figure}[htbp] \centering \begin{tikzpicture}[>=Stealth, scale=1.3] \draw[->, thick] (0.5,0) -- (4.8,0) node[right, font=\small] {坐标}; \foreach \x in {1,2,3,4} { \draw (\x, -0.08) -- (\x, 0.08); \node[below, font=\small] at (\x, -0.12) {$\x$}; }
\draw[queryc, line width=2pt] (1, -0.45) -- (4, -0.45); \fill[queryc] (1, -0.45) circle (2pt); \fill[queryc] (4, -0.45) circle (2pt); \node[queryc, font=\small] at (2.5, -0.8) {查询区间 $[1, 4]$};
\draw[rejc, line width=3.5pt, opacity=0.5] (1, 0.7) -- (4, 0.7); \node[rejc, font=\small, right] at (4.15, 0.7) {$[1,4]$};
\draw[selc, line width=3.5pt] (1, 1.3) -- (3, 1.3); \fill[selc] (1, 1.3) circle (2pt); \fill[selc] (3, 1.3) circle (2pt); \node[selc, font=\small, right] at (4.15, 1.3) {$[1,3]$ \textbf{选}};
\draw[selc, line width=3.5pt] (3, 1.9) -- (4, 1.9); \fill[selc] (3, 1.9) circle (2pt); \fill[selc] (4, 1.9) circle (2pt); \node[selc, font=\small, right] at (4.15, 1.9) {$[3,4]$ \textbf{选}};
\draw[->, jumpc, thick, dashed] (3, 1.1) -- (3, 1.7); \node[jumpc, font=\scriptsize, left] at (2.9, 1.4) {$p \leftarrow 3$}; \end{tikzpicture} \caption{样例查询 $[1, 4]$ 的贪心过程。初始 $p=1$,先选右端点最小的 $[1,3]$,更新 $p=3$;再选 $[3,4]$,答案为 $2$。$[1,4]$ 因右端点更大而被 $[1,3]$ 取代。} \label{fig:greedy} \end{figure}
\section{从暴力到倍增}
\subsection{朴素贪心的瓶颈}
对单次查询,贪心过程需逐个选取列车,最多选 $O(n)$ 辆。面对 $m$ 次查询,总时间 $O(nm)$,在 $n, m \le 10^5$ 下不可接受。瓶颈在于:每次查询都要"一步步跳"。能否预处理后一次跳很多步?
\subsection{确定性的"下一步"与倍增}
贪心过程有一个关键性质:\textbf{下一步选哪辆列车,只取决于当前指针 $p$ 的位置}。$y$ 只决定何时停止跳跃,不影响每一步的选择方向。
因此我们可以把这个"下一步"预处理出来,进而用\textbf{倍增}加速。定义二维数组 $\texttt{nxt}[i][k]$:
\begin{quote} $\texttt{nxt}[i][k]$:从离散化位置 $i$ 出发,贪心放入 $2^k$ 辆列车后,到达的右端点位置。 \end{quote}
基础层 $\texttt{nxt}[i][0]$ 即"从位置 $i$ 出发放 $1$ 辆列车到达的最小右端点",递推层则将两个半步合并: $$\texttt{nxt}[i][k] = \texttt{nxt}[\texttt{nxt}[i][k\!-\!1]][k\!-\!1]$$
含义直观:先跳 $2^{k-1}$ 辆到达中间位置,再从中间位置跳 $2^{k-1}$ 辆,合计 $2^k$ 辆。
图~\ref{fig:lifting} 以样例展示了倍增跳跃如何将两步合并为一步。
\begin{figure}[htbp] \centering \begin{tikzpicture}[>=Stealth] \node[circle, draw, thick, fill=white, minimum size=1cm] (n0) at (0, 0) {$1$}; \node[circle, draw, thick, fill=white, minimum size=1cm] (n1) at (3.5, 0) {$3$}; \node[circle, draw, thick, fill=white, minimum size=1cm] (n2) at (7, 0) {$4$}; \node[circle, draw, thick, dashed, fill=gray!10, minimum size=1cm] (nV) at (10.5, 0) {$\infty$};
\node[below=0.25cm, font=\scriptsize] at (n0.south) {idx $0$}; \node[below=0.25cm, font=\scriptsize] at (n1.south) {idx $1$}; \node[below=0.25cm, font=\scriptsize] at (n2.south) {idx $2$}; \node[below=0.25cm, font=\scriptsize] at (nV.south) {idx $V$(哨兵)};
\draw[->, thick, selc] (n0) to[bend left=25] node[above, font=\scriptsize] {$k\!=\!0$:选 $[1,3]$} (n1); \draw[->, thick, selc] (n1) to[bend left=25] node[above, font=\scriptsize] {$k\!=\!0$:选 $[3,4]$} (n2); \draw[->, thick, rejc] (n2) to[bend left=25] node[above, font=\scriptsize] {$k\!=\!0$:无列车} (nV);
\draw[->, very thick, jumpc, dashed] (n0) to[bend left=50] node[above, font=\small] {$k\!=\!1$:\textbf{一步跳 $2$ 辆}} (n2); \end{tikzpicture} \caption{样例的倍增表。$k=0$ 层(绿色实线)为单步贪心跳跃;$k=1$ 层(红色虚线)将两步合并为一步。查询 $[1,4]$ 时直接用 $k=1$ 一步到位,得到答案 $2$。} \label{fig:lifting} \end{figure}
\section{实现}
\subsection{坐标离散化}
列车端点坐标范围高达 $10^9$,但不同端点值至多 $2n$ 个。代码中使用 \texttt{Compress\_} 模板完成离散化:收集所有列车的 $l_i, r_i$,排序去重后映射为连续索引。
\textbf{查询坐标不参与离散化}——通过 \texttt{projectLr} 方法将原始查询区间 $[x, y]$ 投影到离散化空间。\texttt{lower\_bound} 找到第一个 $\ge x$ 的位置,\texttt{upper\_bound} $- 1$ 找到最后一个 $\le y$ 的位置;若投影为空(\texttt{ok = false}),直接返回 $0$:
\begin{minted}{cpp} ProjectResult projectLr(T l, T r) { int li = (int)(lower_bound(liLs.begin(), liLs.end(), l) - liLs.begin()) + shift; int ri = (int)(upper_bound(liLs.begin(), liLs.end(), r) - liLs.begin()) - 1 + shift; return {li <= ri, li, ri}; } \end{minted}
\subsection{构建基础层 \texttt{dp\_mn\_r}}
$\texttt{dp\_mn\_r}[i]$ 表示所有左端点索引 $\ge i$ 的列车中,右端点索引的最小值。这就是贪心的"下一步"——从位置 $i$ 出发放 $1$ 辆列车能到达的最优位置。
构建方式:将离散化后的列车按左端点排序,对每个位置 $i$ 用二分查找定位该位置的所有列车,取右端点最小值,并从右向左维护后缀最小值:
\begin{minted}{cpp} void gen_dp_l_mn_r() { dp_mn_r.resize(liSZ + 1, INF); auto lr = liLr; sort(all(lr)); for (int i = liSZ - 1; i >= 0; --i) { ll bg = lower_bound(all(lr), array<int, 2>{i, 0}) - lr.begin(); ll ed = lower_bound(all(lr), array<int, 2>{i, INF}) - lr.begin(); dp_mn_r[i] = dp_mn_r[i + 1]; for (int j = bg; j < ed; ++j) { dp_mn_r[i] = min(dp_mn_r[i], lr[j][1]); } } } \end{minted}
\subsection{构建倍增表 \texttt{nxt}}
基础层直接来自 \texttt{dp\_mn\_r},递推层将两个半步合并:
\begin{minted}{cpp} void gen_nxt() { nxt.resize(liSZ, vector<int>(lgSZ + 1, INF)); for (int lay = 0; lay <= lgSZ; ++lay) { for (int i = 0; i < liSZ; ++i) { if (lay == 0) { nxt[i][lay] = dp_mn_r[i]; } else { if (nxt[i][lay - 1] == INF) continue; nxt[i][lay] = nxt[nxt[i][lay - 1]][lay - 1]; } } } } \end{minted}
\subsection{回答查询}
对每个查询 $[x, y]$,先用 \texttt{projectLr} 投影到离散化空间 $[l, r]$,然后从大到小枚举层级 $k$:若跳 $2^k$ 辆后不超过 $r$,则执行跳跃并累加答案:
\begin{minted}{cpp} int reply_query(int bg, int ed) { auto [ok, l, r] = compress.projectLr(bg, ed); if (!ok) return 0; int cur = l; int res = 0; for (int lay = lgSZ; lay >= 0; --lay) { if (nxt[cur][lay] <= r) { cur = nxt[cur][lay]; res += (1 << lay); } } return res; } \end{minted}
\section{复杂度分析}
\begin{itemize} \item \textbf{时间复杂度}:离散化 $O(n \log n)$;\texttt{gen\_dp\_l\_mn\_r} 中排序 $O(n \log n)$,每个位置二分查找后遍历,均摊 $O(n \log n)$;倍增表构建 $O(V \cdot \log V)$($V \le 2n$ 个位置);每次查询 $O(\log V)$。单组数据总计 $O((n + m) \log n)$。 \item \textbf{空间复杂度}:倍增表 $O(V \cdot \log V)$。 \end{itemize}
\section{AC 代码}
\inputminted{cpp}{../src/1005_upsolve_train.cpp}
\end{document}
|