子串能表示从 1 到 N 数字的二进制串 图解题解
这道题到底在问什么
- 输入
- s="0110", n=3
- 输出
- true(1=1、2=10、3=11 都在 0110 里)
- 输入
- s="0110", n=4
- 输出
- false(4=100 在 0110 里找不到)
先想最直接的笨办法
先把 s 等于 100110100 摆好,下标 0 到 8。右边清单列着 1 到 5 各自的二进制,现在全是「待查」。我们一个一个来,把它们的二进制都在 s 里找一遍。(动画第 4 步)
最优解:为什么这么做
一句话答案:LeetCode 1016 子串能表示 1 到 N 的二进制串:用子串检查加剪枝,只验后半段 (n/2, n] 的数是不是 s 的子串,前半段是它们的前缀自动满足。O(n·|s|·log n) 时间、O(log n) 空间。
s 里要翻得出 1 到 n 每个数的二进制
题目给一个只含 0 和 1 的字符串 s 和一个正整数 n。要把 1、2 一直到 n 每个数写成二进制,看它们是不是都能在 s 里找到一段连续字符跟它一模一样——子串就是连续的一段、中间不能断。全找得到返回 true,有一个找不到就 false。题面 s=0110、n=3 返回 true(1、10、11 都在),n=4 返回 false(4 的 100 找不到)。
从 1 挨个查到 n,n 一大这法子还扛得住吗
从 1 到 n 一个个查:把数字转成二进制,去 s 里搜有没有这一段。麻烦在 n 可以很大,逐个搜像没尽头。但有个硬上限压着——(n/2, n] 这半段的数都大于 n/2,两串若从 s 同一处起头,短的必是长的前缀;可这里谁翻倍都越过 n、互不为前缀,就只能各占 s 里一个不同起点。s 一共才 |s| 个起点,n 一旦超过 2 倍 |s| 便塞不下、必有一个落空,直接返回 false。真正要查的 n 顶多 2|s|,两千以内。
为什么只查后半段的数,前半段能整段跳过
更省的一刀是:连 1 到 n/2 这下半段都不用查。任取一个 x≤n/2,把它的二进制末尾不断补 0(等于每次翻倍),迟早会落进 (n/2, n] 得到某个 y;而 bin(x) 正好是 bin(y) 砍掉末尾那几个 0 剩下的前缀。只要 y 的二进制是 s 的子串,它开头那一截 bin(x) 自然也在 s 里。所以下半段每个数都被上半段某个数顺带盖住了,只需验 (n/2, n] 上半段就够,检查量砍半。
先短路,再逐个转二进制拿窗口在 s 上滑
合起来的流程很短。先短路:n 大于 2 倍 |s| 直接判 false。否则从 n 往下数到 n/2+1,逐个取它的最短二进制(去掉 0b 前缀、不带前导零),拿这段定宽的模式在 s 上从左往右滑,一格一格比,看有没有哪一段和它完全相同。中途任意一个数在 s 里滑到头都没配上,立刻返回 false;上半段全部配上,才返回 true。
s=0110、n=3,看 1 是怎么被顺带盖掉的
拿题面 s=0110、n=3 走一遍。2 倍 |s| 是 8,n=3 没超,不短路。n/2=1,要查 3 和 2。先看 3,二进制 11,滑宽 2 的窗口:01 不是、11 对上(下标 1 到 2)。再看 2,二进制 10,滑到下标 2 到 3 截出 10,配上。上半段都在,返回 true——1 全程没查,它的二进制 1 是 2 的 10 的前缀,顺带盖掉。若 n 换成 4,多出的 4 是 100,滑宽 3 只截得出 011、110,没有 100,直接返回 false。
二进制取最短那版,带着前导零就和 s 对不上
几处一错就整道判反。二进制一定取最短那版:Python 用 bin(i)[2:] 去掉 0b、C++ 砍掉前导零;留着前导零就和 s 里的子串对不上,本能找到的数会被漏判。也别只顾逐个查而丢了砍半——下半段是上半段前缀,只验 (n/2, n]。还有几个边界:s 只有一个 0、n=1 时里头没有 1,返回 false;s 里有个 1、n=1 就已满足;n 超过 2 倍 |s| 直接出局。
▶ 动画逐步走查(共 25 步)——想跟着动画一帧帧对照就展开
- 3记住这套:逐个数字转二进制,再用一个等长窗口在 s 上滑动找它。一个找不到就出局,全找到才算 true。下面每帧都在套它。
- 4先把 s 等于 100110100 摆好,下标 0 到 8。右边清单列着 1 到 5 各自的二进制,现在全是「待查」。我们一个一个来,把它们的二进制都在 s 里找一遍。
- 5轮到数字 1,它的二进制是 1,一共 1 位。接下来用一个宽 1 的窗口,从 s 最左边开始往右滑,看哪一段正好是 1。
- 6窗口滑到 [0, 0],框住的是 1,正好等于目标 1,命中!数字 1 在 s 里确实出现,这一段标绿。
- 7数字 1 搞定,它的二进制 1 落在 s 的 [0, 0] 这段。右边清单给 1 打上勾,目前 1 个都找到了,接着查下一个。
- 8轮到数字 2,它的二进制是 10,一共 2 位。接下来用一个宽 2 的窗口,从 s 最左边开始往右滑,看哪一段正好是 10。
- 9窗口滑到 [0, 1],框住的是 10,正好等于目标 10,命中!数字 2 在 s 里确实出现,这一段标绿。
- 10数字 2 搞定,它的二进制 10 落在 s 的 [0, 1] 这段。右边清单给 2 打上勾,目前 2 个都找到了,接着查下一个。
- 11轮到数字 3,它的二进制是 11,一共 2 位。接下来用一个宽 2 的窗口,从 s 最左边开始往右滑,看哪一段正好是 11。
- 12窗口在 [0, 1],框住的是 10,和目标 11 不一样,没命中。窗口整体往右挪一格,继续比。
- 13窗口在 [1, 2],框住的是 00,和目标 11 不一样,没命中。窗口整体往右挪一格,继续比。
- 14窗口在 [2, 3],框住的是 01,和目标 11 不一样,没命中。窗口整体往右挪一格,继续比。
- 15窗口滑到 [3, 4],框住的是 11,正好等于目标 11,命中!数字 3 在 s 里确实出现,这一段标绿。
- 16数字 3 搞定,它的二进制 11 落在 s 的 [3, 4] 这段。右边清单给 3 打上勾,目前 3 个都找到了,接着查下一个。
- 17轮到数字 4,它的二进制是 100,一共 3 位。接下来用一个宽 3 的窗口,从 s 最左边开始往右滑,看哪一段正好是 100。
- 18窗口滑到 [0, 2],框住的是 100,正好等于目标 100,命中!数字 4 在 s 里确实出现,这一段标绿。
- 19数字 4 搞定,它的二进制 100 落在 s 的 [0, 2] 这段。右边清单给 4 打上勾,目前 4 个都找到了,接着查下一个。
- 20轮到数字 5,它的二进制是 101,一共 3 位。接下来用一个宽 3 的窗口,从 s 最左边开始往右滑,看哪一段正好是 101。
- 21窗口在 [0, 2],框住的是 100,和目标 101 不一样,没命中。窗口整体往右挪一格,继续比。
- 22窗口在 [1, 3],框住的是 001,和目标 101 不一样,没命中。窗口整体往右挪一格,继续比。
- 23窗口在 [2, 4],框住的是 011,和目标 101 不一样,没命中。窗口整体往右挪一格,继续比。
- 24窗口在 [3, 5],框住的是 110,和目标 101 不一样,没命中。窗口整体往右挪一格,继续比。
- 25窗口滑到 [4, 6],框住的是 101,正好等于目标 101,命中!数字 5 在 s 里确实出现,这一段标绿。
- 26数字 5 搞定,它的二进制 101 落在 s 的 [4, 6] 这段。右边清单给 5 打上勾,目前 5 个全部找到,没有下一个了,进入总判断、准备返回 true。
- 271 到 5 全都在 s 里找到了二进制对应的子串,清单一个不漏全打了勾。没有任何一个数字落空,所以答案是 true。要是中途有一个找不到,那一刻就该直接返回 false。
⚠️ 容易写错的地方
✗ 错:把 n 直接当成可以无限大去暴力枚举
✓ 对:n 大于 2 倍 s 长度就先返回 false
上半段 (n/2, n] 有超过 |s| 个数,两两互不为二进制前缀(都大于 n/2)、各需 s 里一个不同起点;s 只有 |s| 个起点,n 大于 2 倍 |s| 时装不下、必有数落空、必为 false
✗ 错:二进制串忘了去掉前导零或多余前缀
✓ 对:取最短二进制(Python bin[2:]、C++ 砍前导零)
带前导零的串和真正的子串对不上,会漏判
✗ 错:逐个查 1 到 n 嫌慢却不知能砍半
✓ 对:只查上半段 (n/2, n],下半段是其前缀自动满足
任意 x≤n/2 不断翻倍(末尾补 0)迟早落进上半段得 y,bin(x) 是 bin(y) 删去若干末尾 0 后的前缀,上半段在 s 里前缀自然也在
完整代码(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 TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def queryString(self, s: str, n: int) -> bool:
if n > 2 * len(s):
return False
return all(bin(i)[2:] in s for i in range(n, n // 2, -1))C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#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;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
bool queryString(string s, int n) {
if (n > 2 * (int)s.size()) {
return false;
}
for (int i = n; i > n / 2; --i) {
string b = bitset<32>(i).to_string();
b = b.substr(b.find_first_not_of('0'));
if (s.find(b) == string::npos) {
return false;
}
}
return true;
}
};Java
import java.util.*;
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
public boolean queryString(String s, int n) {
if (n > 2 * s.length()) {
return false;
}
for (int i = n; i > n / 2; i--) {
if (!s.contains(Integer.toBinaryString(i))) {
return false;
}
}
return true;
}
}复杂度
时间
O(n · |s| · log n)
上半段约 n/2 个数(O(n)),每个二进制长 O(log n),在长度 |s| 的串里滑窗逐位比较 O(|s|·log n);|s|≤1000 时可视作常数,实践界约 O(n log n)
空间
O(log n)
只存当前数字的二进制串,长度 O(log n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 子串能表示从 1 到 N 数字的二进制串 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
逐个数字用 indexOf/in 查子串,n 大了复杂度会不会爆?+
不会,关键是 s 的长度锁死在 1000 以内。(n/2, n] 这半段的数两两互不为二进制前缀、各要 s 里一个不同起点,所以 n 一旦超过 2 倍 |s| 就必然放不下、先短路 false;真要枚举的 n 不过两千,每个二进制十位上下,子串匹配也就千次量级,整体很稳。约束把规模钉死了,朴素做法就够。
为什么只查后半段 (n/2, n],前半段真能整段不查?+
任取 x≤n/2,把它二进制末尾不断补 0(每补一个就是翻倍)迟早落进 (n/2, n] 得到 y,而 bin(x) 是 bin(y) 去掉末尾若干 0 后的前缀。y 的二进制既然是 s 的子串,它开头那截 bin(x) 也一定在 s 里。所以前半段每个数都被后半段顺带覆盖,只验后半段不会漏。
还有没有不靠子串匹配的思路?+
有,反着来:把 s 里所有长度不超过 log2(n)+1 的子串都解析成十进制数、丢进一个集合,再看 1 到 n 是不是都在集合里。本质仍是枚举,只是把「数字去找子串」换成「子串生成数字」,适合 s 短、要按数字反查的场景。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 子串能表示从 1 到 N 数字的二进制串 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。