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 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
| \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, calc}
\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{leftc}{RGB}{220,80,60} \definecolor{rightc}{RGB}{50,120,200} \definecolor{winc}{RGB}{0,160,80} \definecolor{losec}{RGB}{160,160,160} \definecolor{reuse}{RGB}{180,80,200}
\title{1004 左右脑互搏 题解} \author{Gospel\_rock} \date{}
\begin{document} \maketitle
\section{题意}
给定一个包含 $n$($1 \le n \le 20$)个正整数的多重集合 $S$。 左脑(先手)和右脑(后手)轮流操作:每次必须从集合中删除一个元素 $x$, 且 $x$ 必须\textbf{严格大于}删除后剩余元素的异或和。 若集合中只剩一个元素,可以直接删去(剩余异或和为 $0$)。 无法操作者判负。双方均采用最优策略,求谁获胜。
\section{分析}
\subsection{博弈建模}
这是一个标准的\textbf{组合博弈}问题。 每个局面(当前集合的状态)在双方最优策略下有确定的胜负:
\begin{itemize} \item \textbf{必胜态}:存在至少一种合法操作,使得对手进入\textbf{必败态}。 \item \textbf{必败态}:所有合法操作都使对手进入\textbf{必胜态};或者根本无法操作。 \end{itemize}
空集是必败态(当前玩家无法操作)。我们从空集出发,自底向上递推出所有状态的胜负。
\subsection{状态表示——为什么需要状压?}
朴素做法是对多重集合进行 DFS 回溯搜索。但问题在于: \textbf{不同的删除顺序可以到达相同的剩余集合}。 例如先删 $a$ 再删 $b$,和先删 $b$ 再删 $a$, 到达的集合完全一样,博弈结论也一样,却被搜了两次。
图~\ref{fig:dag} 展示了 $3$ 个元素 $\{a_0, a_1, a_2\}$ 的状态转移关系。 每个节点是一个子集(用二进制掩码标注), 箭头表示"删除一个元素"的转移。 可以看到 $\{a_0\}$、$\{a_1\}$、$\{a_2\}$ 各自都被\textbf{两条不同的路径}到达。 如果不记忆化,同一个状态会被重复计算多次; 当 $n = 20$ 时,不同的删除排列数量可达 $20!$,远远无法承受。
\begin{figure}[htbp] \centering \begin{tikzpicture}[ >=Stealth, node distance=1.8cm and 2.2cm, state/.style={rectangle, rounded corners=4pt, draw, thick, minimum width=1.6cm, minimum height=0.7cm, align=center, font=\small}, reusearrow/.style={->, thick, reuse, dashed}, normarrow/.style={->, thick, black!70}, ] \node[state, fill=leftc!12] (111) {$111$\\[-2pt]{\scriptsize $\{a_0,a_1,a_2\}$}};
\node[state, fill=rightc!12, below left=1.6cm and 2.2cm of 111] (011) {$011$\\[-2pt]{\scriptsize $\{a_0,a_1\}$}}; \node[state, fill=rightc!12, below=1.6cm of 111] (101) {$101$\\[-2pt]{\scriptsize $\{a_0,a_2\}$}}; \node[state, fill=rightc!12, below right=1.6cm and 2.2cm of 111] (110) {$110$\\[-2pt]{\scriptsize $\{a_1,a_2\}$}};
\node[state, fill=leftc!12, below left=1.6cm and 2.2cm of 101] (001) {$001$\\[-2pt]{\scriptsize $\{a_0\}$}}; \node[state, fill=leftc!12, below=1.6cm of 101] (010) {$010$\\[-2pt]{\scriptsize $\{a_1\}$}}; \node[state, fill=leftc!12, below right=1.6cm and 2.2cm of 101] (100) {$100$\\[-2pt]{\scriptsize $\{a_2\}$}};
\node[state, fill=losec!30, below=1.6cm of 010] (000) {$000$\\[-2pt]{\scriptsize $\varnothing$}};
\draw[normarrow] (111) -- node[above left, font=\scriptsize] {删 $a_2$} (011); \draw[normarrow] (111) -- node[left, font=\scriptsize] {删 $a_1$} (101); \draw[normarrow] (111) -- node[above right, font=\scriptsize] {删 $a_0$} (110);
\draw[normarrow] (011) -- node[left, font=\scriptsize] {删 $a_1$} (001); \draw[normarrow] (110) -- node[right, font=\scriptsize] {删 $a_1$} (100); \draw[normarrow] (101) -- node[pos=0.2, left, font=\scriptsize] {删 $a_2$} (001); \draw[normarrow] (011) -- node[pos=0.2, right, font=\scriptsize] {删 $a_0$} (010); \draw[normarrow] (110) -- node[pos=0.2, left, font=\scriptsize] {删 $a_2$} (010); \draw[normarrow] (101) -- node[pos=0.2, right, font=\scriptsize] {删 $a_0$} (100);
\draw[normarrow] (001) -- (000); \draw[normarrow] (010) -- (000); \draw[normarrow] (100) -- (000);
\node[reuse, font=\small\bfseries, align=center, right=0.5cm of 100] {多条路径\\汇聚!}; \end{tikzpicture} \caption{$3$ 个元素的子集状态转移 DAG(忽略操作合法性)。 同一状态可从多条路径到达,如 $\{a_0\}$ 同时被 $\{a_0,a_1\}$ 和 $\{a_0,a_2\}$ 转移到。 用掩码标识状态后,每个状态只需计算一次。} \label{fig:dag} \end{figure}
解决方案:$n \le 20$,用一个 $n$ 位的\textbf{二进制掩码}(bitmask)表示 "哪些元素还在集合中"。共 $2^n$ 种状态,对每个状态记忆化搜索结果即可。
注意到轮到谁操作也不需要额外记录——已经删除了 $n - \texttt{popcount}(\mathit{mask})$ 个元素,其奇偶性直接决定当前是左脑还是右脑。
\subsection{DFS 的设计:用一行表达极小极大}
博弈搜索的核心在于 \texttt{dfs(sta)} 的返回值定义和递归逻辑。 我们令 \texttt{dfs(sta)} 返回 $1$ 表示"当前操作者必胜",$0$ 表示"当前操作者必败"。 则关键的一行代码为:
\begin{minted}{cpp} res |= dfs(sta - (1ll << i)) ^ 1; \end{minted}
这一行同时完成了两件事:
\begin{itemize} \item \textbf{视角翻转}(\texttt{\^{} 1}):\texttt{dfs} 返回的是对手的胜负, 而我们关心的是"这步操作对\textbf{我}是否有利"。 对手必败(返回 $0$)意味着我必胜($0 \oplus 1 = 1$); 对手必胜(返回 $1$)意味着我必败($1 \oplus 1 = 0$)。 \item \textbf{存在性判定}(\texttt{|=}):只要\textbf{存在}某个操作使对手进入必败态, \texttt{res} 就被置为 $1$,此后不论其它操作结果如何都不会再变回 $0$。 这正是"必胜态 $\iff$ 存在至少一步使对手进入必败态"的直接编码。 \end{itemize}
若所有合法操作都找不到让对手必败的,\texttt{res} 保持初始值 $0$,即当前状态为必败态。 这也自然涵盖了"无合法操作"的情况(循环体不执行,直接返回 $0$)。
图~\ref{fig:dfs} 展示了这一过程:当前状态 $S$ 有三种合法操作,分别转移到 $S_1, S_2, S_3$。 递归返回的是\textbf{对手视角}下的胜负,经 \texttt{\^{} 1} 翻转为我方视角后, 再用 \texttt{|=} 汇总——只要有一个 $1$,当前状态就是必胜态。
\begin{figure}[htbp] \centering \begin{tikzpicture}[ >=Stealth, wnode/.style={rectangle, rounded corners=4pt, draw, thick, fill=winc!15, minimum width=1.4cm, minimum height=0.7cm, align=center, font=\small}, lnode/.style={rectangle, rounded corners=4pt, draw, thick, fill=leftc!15, minimum width=1.4cm, minimum height=0.7cm, align=center, font=\small}, flipbox/.style={rectangle, rounded corners=2pt, draw, thick, dashed, fill=yellow!12, minimum width=0.9cm, minimum height=0.5cm, font=\small}, orbox/.style={rectangle, rounded corners=4pt, draw=winc, very thick, fill=winc!8, minimum width=2.8cm, minimum height=0.7cm, font=\small}, ] \node[wnode, minimum width=2.4cm, minimum height=0.9cm] (S) {状态 $S$\\[-1pt]{\scriptsize\texttt{res = ?}}};
\node[lnode, below left=2.0cm and 2.8cm of S] (S1) {$S_1$\\[-1pt]{\scriptsize 对手\textbf{必胜}}}; \node[wnode, below=2.0cm of S] (S2) {$S_2$\\[-1pt]{\scriptsize 对手\textbf{必败}}}; \node[lnode, below right=2.0cm and 2.8cm of S] (S3) {$S_3$\\[-1pt]{\scriptsize 对手\textbf{必胜}}};
\node[font=\scriptsize, below=0.15cm of S1] (r1) {返回 $1$}; \node[font=\scriptsize, below=0.15cm of S2] (r2) {返回 $0$}; \node[font=\scriptsize, below=0.15cm of S3] (r3) {返回 $1$};
\node[flipbox, below left=0.8cm and 0.9cm of S] (f1) {$\oplus 1$}; \node[flipbox, below=1.0cm of S] (f2) {$\oplus 1$}; \node[flipbox, below right=0.8cm and 0.9cm of S] (f3) {$\oplus 1$};
\node[font=\small\bfseries, text=leftc, left=0.15cm of f1] {$0$}; \node[font=\small\bfseries, text=winc, left=0.15cm of f2] {$1$}; \node[font=\small\bfseries, text=leftc, right=0.15cm of f3] {$0$};
\draw[->, thick] (S) -- node[above left, font=\scriptsize] {删 $a_i$} (f1); \draw[->, thick] (S) -- node[left, font=\scriptsize] {删 $a_j$} (f2); \draw[->, thick] (S) -- node[above right, font=\scriptsize] {删 $a_k$} (f3); \draw[->, thick, black!50] (f1) -- (S1); \draw[->, thick, black!50] (f2) -- (S2); \draw[->, thick, black!50] (f3) -- (S3);
\draw[->, thick, winc, densely dashed] (f1.north) to[bend left=25] node[above, font=\scriptsize, text=black] {} (S.south west); \draw[->, thick, winc, densely dashed] (f2.north) -- (S.south); \draw[->, thick, winc, densely dashed] (f3.north) to[bend right=25] node[above, font=\scriptsize, text=black] {} (S.south east);
\node[orbox, right=1.5cm of S] (result) {\texttt{res = 0\,|\,0\,|\,1\,|\,0 = \textbf{1}}}; \draw[->, thick, winc] (S.east) -- (result.west);
\node[font=\small, text=winc!70!black, below=3.6cm of S] {存在一条路使对手必败 $\Longrightarrow$ 当前状态为\textbf{必胜态}}; \end{tikzpicture} \caption{\texttt{res |= dfs(...) \^{} 1} 的工作原理。递归返回对手视角的胜负,经 $\oplus 1$ 翻转为我方视角,再用 \texttt{|=} 汇总。只要有一个分支翻转后为 $1$,当前状态即为必胜。} \label{fig:dfs} \end{figure}
\subsection{完整算法流程}
对状态 $\mathit{mask}$,执行如下递归:
\begin{enumerate} \item 计算当前集合的异或和 $S = \bigoplus_{i:\, \mathit{mask} \text{ 的第 } i \text{ 位为 } 1} a_i$。 \item 枚举 $\mathit{mask}$ 中每个为 $1$ 的位 $i$,若 $a_i > S \oplus a_i$, 则尝试删除 $a_i$,递归求解 $\mathit{mask} \setminus \{i\}$。 \item 若存在某次删除使对手进入必败态(\texttt{res} 被 \texttt{|=} 置 $1$), 则当前状态为\textbf{必胜态};否则为\textbf{必败态}。 \item 将结果存入 $\texttt{memo}[\mathit{mask}]$ 以避免重复计算。 \end{enumerate}
\section{样例推演}
以样例 $2$($\{2, 3, 6, 8\}$)为例,图~\ref{fig:game} 展示了完整的博弈过程。 由于每一步恰好只有一种合法操作,博弈路径是唯一确定的。
\begin{figure}[htbp] \centering \begin{tikzpicture}[ >=Stealth, state/.style={rectangle, rounded corners=4pt, draw, thick, minimum width=2.8cm, minimum height=1.1cm, align=center, font=\small}, every edge/.style={draw, ->, thick}, ] \node[state, fill=leftc!12] (s0) {$\{2,3,6,8\}$\\[-1pt]{\scriptsize 异或和 $= 15$}}; \node[state, fill=rightc!12, below=1.3cm of s0] (s1) {$\{2,3,6\}$\\[-1pt]{\scriptsize 异或和 $= 7$}}; \node[state, fill=leftc!12, below=1.3cm of s1] (s2) {$\{2,3\}$\\[-1pt]{\scriptsize 异或和 $= 1$}}; \node[state, fill=rightc!12, below=1.3cm of s2] (s3) {$\{2\}$\\[-1pt]{\scriptsize 异或和 $= 2$}}; \node[state, fill=losec!30, below=1.3cm of s3] (s4) {$\varnothing$};
\draw[->] (s0) -- node[right, font=\small, text=leftc] {左脑删 $8$\;($8 > 15 \oplus 8 = 7$ \checkmark)} (s1); \draw[->] (s1) -- node[right, font=\small, text=rightc] {右脑删 $6$\;($6 > 7 \oplus 6 = 1$ \checkmark)} (s2); \draw[->] (s2) -- node[right, font=\small, text=leftc] {左脑删 $3$\;($3 > 1 \oplus 3 = 2$ \checkmark)} (s3); \draw[->] (s3) -- node[right, font=\small, text=rightc] {右脑删 $2$\;($2 > 0$ \checkmark)} (s4);
\node[winc, font=\bfseries, right=0.5cm of s4] {右脑获胜!};
\node[state, fill=leftc!12, minimum width=1.2cm, minimum height=0.5cm] at (5.5, 0) {\scriptsize 左脑回合}; \node[state, fill=rightc!12, minimum width=1.2cm, minimum height=0.5cm] at (5.5, -0.8) {\scriptsize 右脑回合}; \end{tikzpicture} \caption{样例 $2$($\{2,3,6,8\}$)的博弈过程。每步只有唯一合法操作,共 $4$ 步,最后一步由右脑完成,右脑获胜。} \label{fig:game} \end{figure}
样例 $3$($\{2, 2\}$)则更简单:当前异或和为 $2 \oplus 2 = 0$, 左脑若删去一个 $2$,剩余异或和为 $2$,不满足 $2 > 2$, 因此左脑\textbf{无法进行任何操作},直接判负。
\section{复杂度分析}
\begin{itemize} \item \textbf{时间复杂度}:共 $2^n$ 个状态,每个状态枚举 $O(n)$ 个元素尝试转移, 总计 $O(2^n \cdot n)$。当 $n = 20$ 时约为 $2 \times 10^7$,可以通过。 \item \textbf{空间复杂度}:$O(2^n)$ 用于存储记忆化数组。 \end{itemize}
\section{AC 代码}
\inputminted{cpp}{../src/1004_upsolve.cpp}
\end{document}
|