漂亮数组 图解题解
这道题到底在问什么
- 输入
- n = 5
- 输出
- [1, 5, 3, 2, 4]
最优解:一步一步想明白
- 3记住这句话,后面每帧都在套它:奇数全靠左、偶数全靠右,跨中点必安全;两侧内部交给规模减半的子问题递归解决。
- 4从最小规模起步。n=1 时只有一个数 1,孤零零一个数当然漂亮,它是整套递归的地基。后面所有更大的漂亮数组,都从它一层层搭上来。
- 5现在搭 n=2。奇数个数是 1 个、偶数个数是 1 个,所以奇侧和偶侧都拿地基 f(1)=[1] 当素材。下面把它分别映射成「全奇」和「全偶」。
- 6奇侧用映射 2x-1。把素材里的 1 代进去,2 乘 1 减 1 还是 1,这是一个奇数。奇侧得到 [1]。
- 7偶侧用映射 2x。同样把 1 代进去,2 乘 1 等于 2,这是一个偶数。偶侧得到 [2]。
- 8奇侧放左、偶侧放右,拼成 [1, 2]。这就是 f(2),存进右边的备忘录,后面更大的规模会反复取用它。
- 9验一下:只有 1 和 2 这一对,中间根本没有别的数能当中点,自然没法违规。f(2) 漂亮,稳。
- 10搭 n=3。1 到 3 里奇数有 2 个、偶数有 1 个,所以奇侧拿 f(2)=[1, 2],偶侧拿 f(1)=[1]。素材都已经是漂亮的,放心映射。
- 11奇侧逐个映射。第一个素材是 1,代进 2x-1 得 1。奇侧第一格填好,先是 [1, 空]。
- 12第二个素材是 2,代进 2x-1 得 3,又是个奇数。奇侧凑齐成 [1, 3],两个都是奇数,符合预期。
- 13偶侧素材是 f(1)=[1],映射 2x 得 2。偶侧只有一格,就是 [2]。
- 14奇侧 [1, 3] 放左、偶侧 [2] 放右,拼成 [1, 3, 2]。这就是 f(3),同样存进备忘录。
- 15检验唯一的三元组:两端是 1 和 2,中点是 3。2 乘中点 3 等于 6,两端之和才 3,根本不等。f(3) 漂亮。
- 16终于到目标 n=5。1 到 5 里奇数 3 个、偶数 2 个,奇侧拿 f(3)=[1, 3, 2],偶侧拿 f(2)=[1, 2]。两份素材备忘录里都现成,直接映射。
- 17奇侧映射开始。素材第一个 1,代进 2x-1 得 1。奇侧先填 [1, 空, 空]。
- 18素材第二个 3,代进 2x-1 得 5。奇侧填到 [1, 5, 空]。注意得到的都是奇数,这正是「奇侧」名字的由来。
- 19素材第三个 2,代进 2x-1 得 3。奇侧凑齐 [1, 5, 3],三个全是奇数。左半部分搞定。
- 20换到偶侧,素材是 f(2)=[1, 2]。第一个 1 代进 2x 得 2。偶侧先填 [2, 空]。
- 21第二个 2 代进 2x 得 4。偶侧凑齐 [2, 4],两个都是偶数。右半部分也搞定。
- 22奇侧 [1, 5, 3] 放左、偶侧 [2, 4] 放右,拼成 [1, 5, 3, 2, 4]。左边三个全奇、右边两个全偶,这就是 n=5 的漂亮数组。
- 23看为什么对。只要一对位置跨过了「奇偶分界线」,左端是奇数、右端是偶数,两端之和是奇数;而 2 乘任何中点都是偶数,一奇一偶绝不可能相等。所有跨界三元组天然安全。
- 24那同一侧内部呢?左边这三个奇数,本质就是 f(3) 做了一次 2x-1 的整体变换。这种线性变换不会破坏「漂亮」性质,而 f(3) 我们已经验过是漂亮的,所以左侧内部也安全。偶侧同理。
- 25两块拼起来:跨界的三元组安全,各侧内部也安全,整个数组就是漂亮的。n=5 的答案锁定为 [1, 5, 3, 2, 4]。题目允许任一漂亮数组,这一个就行。
⚠️ 容易写错的地方
✗ 错:奇偶映射搞反
✓ 对:奇侧用 2x-1(得奇数)、偶侧用 2x(得偶数)
对调后跨界仍是一奇一偶、护城河不塌;真坑是 2x-1 须配奇数个数 ceil、2x 须配偶数个数 floor,规模配错会生成越界/重复值、拼不满 1 到 n 的排列
✗ 错:子问题规模分错
✓ 对:奇侧取 ceil(n/2)、偶侧取 floor(n/2)
它们正好对应 1 到 n 里奇数、偶数的个数,分错会漏数或重数
✗ 错:以为要 O(n³) 暴力校验每个三元组
✓ 对:构造法天然保证漂亮,无需校验
奇左偶右的隔断 + 子问题递归已证明正确,加校验纯属多余
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def beautifulArray(self, n: int) -> List[int]:
if n == 1:
return [1]
left = self.beautifulArray((n + 1) >> 1)
right = self.beautifulArray(n >> 1)
left = [x * 2 - 1 for x in left]
right = [x * 2 for x in right]
return left + rightC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
vector<int> beautifulArray(int n) {
if (n == 1) return {1};
vector<int> left = beautifulArray((n + 1) >> 1);
vector<int> right = beautifulArray(n >> 1);
vector<int> ans(n);
int i = 0;
for (int& x : left) ans[i++] = x * 2 - 1;
for (int& x : right) ans[i++] = x * 2;
return ans;
}
};Java
import java.util.*;
class Solution {
public int[] beautifulArray(int n) {
if (n == 1) {
return new int[] {1};
}
int[] left = beautifulArray((n + 1) >> 1);
int[] right = beautifulArray(n >> 1);
int[] ans = new int[n];
int i = 0;
for (int x : left) {
ans[i++] = x * 2 - 1;
}
for (int x : right) {
ans[i++] = x * 2;
}
return ans;
}
}复杂度
时间
O(n log n)
无备忘录;每层合并共 O(n),递归约 log n 层
空间
O(n)
输出+临时数组峰值 O(n),递归栈 O(log n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 漂亮数组 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么把漂亮数组每个元素做 2x-1 或 2x 这种线性变换后,它依然漂亮?+
漂亮的判定只关心 2×A[k] = A[i] + A[j] 这个等式。对整个数组做仿射变换 x → ax+b,等式两边会同步变成 2(a·A[k]+b) 和 (a·A[i]+b)+(a·A[j]+b),化简后等价于原来的 2·A[k] = A[i]+A[j]。也就是说,违规与否在线性变换下不变。所以原数组漂亮,变换后仍然漂亮,这正是 2x-1、2x 能放心用的根据。
这道题怎么想到用分治?+
突破口是那个奇偶观察:把奇数全堆左、偶数全堆右,就能一刀隔断所有跨中点的等差三元组。一旦能把问题「沿中线切成互不干扰的两半」,再让每半递归解决规模约 n/2 的同类子问题,这就是典型的分治信号。先找到「能让两半解耦的切法」,分治自然浮现。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 漂亮数组 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。