普通视图
欧航局与摩纳哥公司合作开展月球车技术研究
eBay将裁减约6%员工
OpenAI将把伦敦打造成其美国以外最大的研究中心
谷歌据悉与Meta达成价值数十亿美元的AI芯片交易
戴尔科技单季营收利润创历史新高,宣布现金股息上调20%
库克暗示苹果将于周一上午举行发布会
毫秒级响应:前端本地搜索的“降维打击”
你一定被原生 IndexedDB 的查询限制气笑过:它只支持前缀匹配(IDBKeyRange.bound),想搜“中间字符”?对不起,原生没有 LIKE %keyword%。
在 AI Prompt Manager 场景下,用户可能搜“前端”、“报告”、“审计”,这些词可能出现在标题中间。如果直接 getAll() 拿出来在 JS 里 filter,数据量上万时,内存占用和主线程卡顿会直接让你的“资深”头衔蒙尘。
1. 为什么原生查询不行?
原生 IndexedDB 的索引是 B-Tree 结构,它能极快地找到“以某字符开头”的数据,但无法处理“包含某字符”。
-
低级做法:
getAll()+Array.prototype.filter。数据量 10k+ 时,解析 JSON 的开销会让 UI 瞬间掉帧。 - 高级做法:倒排索引(Inverted Index) 。
2. 方案一:引入 FlexSearch (极致性能的首选)
FlexSearch 是目前 Web 端最快的全量搜索库,它的速度比 Fuse.js 快一个数量级。
实战集成:PromptDB + FlexSearch
JavaScript
import { Index } from "flexsearch";
class SearchablePromptDB extends PromptDB {
constructor() {
super();
// 创建内存索引,开启“模糊匹配”(suggest)
this.index = new Index({
tokenize: "forward", // 适合前缀+中间搜索
resolution: 9,
cache: true
});
}
// 1. 同步索引:在数据存入 DB 的同时,存入 FlexSearch
async addWithIndex(prompt) {
await this.set(prompt);
this.index.add(prompt.id, `${prompt.title} ${prompt.content}`);
}
// 2. 毫秒级搜索
search(query) {
const results = this.index.search(query, { limit: 20 });
// results 返回的是 id 数组,再去 DB 拿具体对象(或从内存缓存拿)
return results;
}
}
3. 方案二:手写“倒排索引” (不依赖库的深度方案)
如果你不想增加包体积,可以利用 IndexedDB 的 multiEntry 特性。
核心技巧:词根索引化
将 Prompt 的标题和标签拆分成字/词,存入一个专门的 searchTerms 数组字段。
JavaScript
// 存入时
const prompt = {
id: 'p1',
title: '金融审计助手',
// 手动分词:['金', '融', '审', '计', '助', '手', '金融', '审计']
searchKeywords: splitWords('金融审计助手')
};
// 在 DB 初始化时,为这个数组字段开启 multiEntry
store.createIndex('keywords_idx', 'searchKeywords', { multiEntry: true });
// 查询时
const range = IDBKeyRange.only('审计');
const request = index.getAll(range); // 瞬发响应,因为它是原生索引
注:multiEntry: true 会为数组中的每个元素在 B-Tree 中创建一个独立的指针。
4. 性能瓶颈的终极解决方案:Web Worker
即便搜索算法再快,当数据量达到 10 万级,字符串分词和索引构建依然会占用主线程。
架构设计:将搜索推入边缘
- 主线程:只负责接收用户输入和渲染结果。
- Worker 线程:持有 FlexSearch 实例,监听 IndexedDB 的变化。
- 流程:输入 -> PostMessage -> Worker 搜索 -> 返回 ID 列表 -> 主线程渲染。
JavaScript
// search.worker.js
self.onmessage = ({ data: { type, payload } }) => {
if (type === 'SEARCH') {
const results = flexIndex.search(payload);
self.postMessage(results);
}
};
5. 优化建议
-
分词策略:中文搜索最简单有效的是 “二元分词” 。例如“我的代码”,拆分为
['我', '的', '代', '码', '我的', '的代码']。 - 防抖 (Debounce) :搜索框必须加 150-300ms 防抖,避免用户打字太快导致 Worker 任务堆积。
-
结果高亮:搜索是毫秒级的,但千万别忘了在 UI 上用
<mark>标签高亮匹配字符,这才是“体感”流畅的关键。 - 分片加载:搜索结果如果太多,只取前 20 条,配合滚动加载。
存储配额:用 navigator.storage.estimate() 预判浏览器什么时候会删你的数据
你一定知道浏览器是个“无情的房东”。当磁盘空间不足时,浏览器会自动启动 驱逐机制(Eviction) ,而你的 IndexedDB 数据往往是第一批被清理的对象,且清理前没有任何弹窗提示。
为了不让用户的 AI Prompt 模板一夜之间消失,我们需要利用 navigator.storage API 进行“生存预判”。
1. 核心数据:Quota vs. Usage
通过 navigator.storage.estimate(),我们可以获取到当前域名的存储状态:
-
usage: 你已经占用了多少字节(Byte)。 -
quota: 浏览器分配给你的最高额度。通常是磁盘剩余空间的 80% ,但这只是“软上限”。
实战代码封装
JavaScript
async function checkStorageHealth() {
if (!navigator.storage || !navigator.storage.estimate) {
return { status: 'unsupported' };
}
const { usage, quota } = await navigator.storage.estimate();
// 转换为更直观的 GB/MB
const usageMB = (usage / (1024 * 1024)).toFixed(2);
const quotaMB = (quota / (1024 * 1024)).toFixed(2);
const percentUsed = ((usage / quota) * 100).toFixed(2);
return {
usageMB: `${usageMB}MB`,
quotaMB: `${quotaMB}MB`,
percentUsed: `${percentUsed}%`,
isRisk: percentUsed > 80 // 占用超过 80% 即为高风险
};
}
// 在 AI Prompt Manager 初始化时调用
checkStorageHealth().then(console.table);
2. 存储策略:最佳努力 (Best-effort) vs. 持久化 (Persistent)
默认情况下,所有的 Web 存储都是 “最佳努力(Best-effort)” 。这意味着当用户电脑没空间时,浏览器会根据 LRU(最近最少使用)原则删掉你的数据库。
作为资深开发,你应该在用户存储了重要数据后,申请 “持久化存储权限” :
JavaScript
async function requestPersistence() {
if (navigator.storage && navigator.storage.persist) {
// 检查是否已经持久化
const isPersisted = await navigator.storage.persisted();
if (isPersisted) return true;
// 申请持久化
const granted = await navigator.storage.persist();
return granted; // 返回 true 表示浏览器承诺:除非用户手动清理,否则绝不删除
}
return false;
}
注意: 浏览器(尤其是 Chrome)会自动根据网站的“活跃度”决定是否授予持久化权限。如果你的应用被用户频繁访问,获批概率极大。
3. 浏览器如何决定删谁?(驱逐逻辑)
不同的房东有不同的驱逐规矩:
| 浏览器 | 存储上限 (Quota) | 驱逐触发条件 |
|---|---|---|
| Chrome / Edge | 共享磁盘剩余空间的 80% | 全局磁盘空间不足 10% 或 2GB 时 |
| Firefox | 磁盘剩余空间的 80% | 超过总额度的 10% 时开始 LRU 清理 |
| Safari | 严格限制(通常 1GB 或更少) | 7 天不使用即可能被清理(移动端更严) |
4. “预判避坑”指南
-
静默失败的处理:不要等到
set()报错QuotaExceededError才行动。在那之前,通过estimate()监控,当percentUsed > 70%时,在 UI 上给用户一个“清理旧数据”的黄色警告。 -
垃圾回收的滞后:当你删除了 100MB 的 IndexedDB 数据后,
usage不会立刻减少。浏览器需要时间进行内部清理(Compaction),所以不要在delete之后立刻测estimate。 -
计算并不精准:
estimate()返回的是字节数,但 IndexedDB 存储时会有额外的索引开销和数据库元数据。实际占用通常比数据本身大 10% 到 20% 。 -
金融数据备份建议:既然涉及到金融级别的安全性,永远不要把浏览器存储作为唯一的真理来源。
- 低频:将重要 Prompt 同步到后端云端。
-
高频:提供本地导出
.json的功能作为手动备份。
长和:对巴拿马最高法院裁决及巴拿马之相应行动表示强烈反对
央行:将综合考虑人民币离岸市场发展、跨境资本流动形势等因素,适时调整参数
央行:将根据实践情况,及时完善政策、优化展业指引
央行:人民币账户融资比例上限的规定同时废止
央行:当人民币跨境同业融资净融出余额达到上限的80%时,应进行预警提示
央行:支持境内银行顺应市场需求,按照依法合规、风险可控的原则开展人民币跨境同业融资业务
美股三大指数收盘涨跌不一,英伟达跌超5%
每日一题-使二进制字符串全为 1 的最少操作次数🔴
给你一个二进制字符串 s 和一个整数 k。
在一次操作中,你必须选择 恰好 k 个 不同的 下标,并将每个 '0' 翻转 为 '1',每个 '1' 翻转为 '0'。
返回使字符串中所有字符都等于 '1' 所需的 最少 操作次数。如果不可能,则返回 -1。
示例 1:
输入: s = "110", k = 1
输出: 1
解释:
-
s中有一个'0'。 - 由于
k = 1,我们可以直接在一次操作中翻转它。
示例 2:
输入: s = "0101", k = 3
输出: 2
解释:
每次操作选择 k = 3 个下标的一种最优操作方案是:
-
操作 1:翻转下标
[0, 1, 3]。s从"0101"变为"1000"。 -
操作 2:翻转下标
[1, 2, 3]。s从"1000"变为"1111"。
因此,最少操作次数为 2。
示例 3:
输入: s = "101", k = 2
输出: -1
解释:
由于 k = 2 且 s 中只有一个 '0',因此不可能通过翻转恰好 k 个位来使所有字符变为 '1'。因此,答案是 -1。
提示:
1 <= s.length <= 105-
s[i]的值为'0'或'1'。 1 <= k <= s.length
两种方法:BFS / 数学(Python/Java/C++/Go)
方法一:BFS
做法和 2612. 最少翻转操作数 是类似的,请先阅读 我的题解。
设 $s$ 的长度为 $n$,其中有 $z$ 个 $0$。
翻转一次后,$s$ 有多少个 $0$?$z$ 可以变成什么数?
设翻转了 $x$ 个 $0$,那么也同时翻转了 $k-x$ 个 $1$,这些 $1$ 变成了 $0$。
所以 $z$ 减少了 $x$,然后又增加了 $k-x$。
所以新的 $z'$ 为
$$
z' = z - x + (k-x) = z+k-2x
$$
$x$ 最大可以是 $k$,但这不能超过 $s$ 中的 $0$ 的个数 $z$,所以 $x$ 最大为 $\min(k,z)$。
$k-x$ 最大可以是 $k$,但这不能超过 $s$ 中的 $1$ 的个数 $n-z$,所以 $k-x$ 最大为 $\min(k,n-z)$,所以 $x$ 最小为 $\max(0,k-n+z)$。
所以 $x$ 的范围为
$$
[\max(0,k-n+z),\min(k,z)]
$$
其余逻辑同 2612 题。
###py
class Solution:
def minOperations(self, s: str, k: int) -> int:
n = len(s)
not_vis = [SortedList(range(0, n + 1, 2)), SortedList(range(1, n + 1, 2))]
not_vis[0].add(n + 1) # 哨兵,下面 sl[idx] <= mx 无需判断越界
not_vis[1].add(n + 1)
start = s.count('0') # 起点
not_vis[start % 2].discard(start)
q = [start]
ans = 0
while q:
tmp = q
q = []
for z in tmp:
if z == 0: # 没有 0,翻转完毕
return ans
# not_vis[mn % 2] 中的从 mn 到 mx 都可以从 z 翻转到
mn = z + k - 2 * min(k, z)
mx = z + k - 2 * max(0, k - n + z)
sl = not_vis[mn % 2]
idx = sl.bisect_left(mn)
while sl[idx] <= mx:
j = sl.pop(idx) # 注意 pop(idx) 会使后续元素向左移,不需要写 idx += 1
q.append(j)
ans += 1
return -1
###java
class Solution {
public int minOperations(String s, int k) {
int n = s.length();
TreeSet<Integer>[] notVis = new TreeSet[2];
for (int m = 0; m < 2; m++) {
notVis[m] = new TreeSet<>();
for (int i = m; i <= n; i += 2) {
notVis[m].add(i);
}
}
// 计算起点
int start = 0;
for (int i = 0; i < n; i++) {
if (s.charAt(i) == '0') {
start++;
}
}
notVis[start % 2].remove(start);
List<Integer> q = List.of(start);
for (int ans = 0; !q.isEmpty(); ans++) {
List<Integer> tmp = q;
q = new ArrayList<>();
for (int z : tmp) {
if (z == 0) { // 没有 0,翻转完毕
return ans;
}
// notVis[mn % 2] 中的从 mn 到 mx 都可以从 z 翻转到
int mn = z + k - 2 * Math.min(k, z);
int mx = z + k - 2 * Math.max(0, k - n + z);
TreeSet<Integer> set = notVis[mn % 2];
for (Iterator<Integer> it = set.tailSet(mn).iterator(); it.hasNext(); it.remove()) {
int j = it.next();
if (j > mx) {
break;
}
q.add(j);
}
}
}
return -1;
}
}
###cpp
class Solution {
public:
int minOperations(string s, int k) {
int n = s.size();
set<int> not_vis[2];
for (int m = 0; m < 2; m++) {
for (int i = m; i <= n; i += 2) {
not_vis[m].insert(i);
}
not_vis[m].insert(n + 1); // 哨兵,下面无需判断 it != st.end()
}
int start = ranges::count(s, '0'); // 起点
not_vis[start % 2].erase(start);
vector<int> q = {start};
for (int ans = 0; !q.empty(); ans++) {
vector<int> nxt;
for (int z : q) {
if (z == 0) { // 没有 0,翻转完毕
return ans;
}
// not_vis[mn % 2] 中的从 mn 到 mx 都可以从 z 翻转到
int mn = z + k - 2 * min(k, z);
int mx = z + k - 2 * max(0, k - n + z);
auto& st = not_vis[mn % 2];
for (auto it = st.lower_bound(mn); *it <= mx; it = st.erase(it)) {
nxt.push_back(*it);
}
}
q = move(nxt);
}
return -1;
}
};
###go
// import "github.com/emirpasic/gods/v2/trees/redblacktree"
func minOperations(s string, k int) (ans int) {
n := len(s)
notVis := [2]*redblacktree.Tree[int, struct{}]{}
for m := range notVis {
notVis[m] = redblacktree.New[int, struct{}]()
for i := m; i <= n; i += 2 {
notVis[m].Put(i, struct{}{})
}
notVis[m].Put(n+1, struct{}{}) // 哨兵,下面无需判断 node != nil
}
start := strings.Count(s, "0")
notVis[start%2].Remove(start)
q := []int{start}
for q != nil {
tmp := q
q = nil
for _, z := range tmp {
if z == 0 { // 没有 0,翻转完毕
return ans
}
// notVis[mn % 2] 中的从 mn 到 mx 都可以从 z 翻转到
mn := z + k - 2*min(k, z)
mx := z + k - 2*max(0, k-n+z)
t := notVis[mn%2]
for node, _ := t.Ceiling(mn); node.Key <= mx; node, _ = t.Ceiling(mn) {
q = append(q, node.Key)
t.Remove(node.Key)
}
}
ans++
}
return -1
}
复杂度分析
- 时间复杂度:$\mathcal{O}(n\log n)$,其中 $n$ 是 $s$ 的长度。$[0,n]$ 中的每个数至多入队出队各一次,每次 $\mathcal{O}(\log n)$ 时间。
- 空间复杂度:$\mathcal{O}(n)$。
方法二:数学
分析
设 $s$ 中有 $z$ 个 $0$,设一共操作了 $m$ 次。那么总翻转次数为 $mk$。
这 $z$ 个 $0$ 必须翻转奇数次,其余 $n-z$ 个 $1$ 必须翻转偶数次。
总翻转次数减去 $z$,剩下每个位置都必须翻转偶数次,所以
$$
mk-z\ 是偶数
$$
下面计算 $m$ 的下界。只要能证明 $m$ 可以等于下界,问题就解决了。
要想把 $z$ 个 $0$ 变成 $1$,总翻转次数至少要是 $z$,即
$$
mk\ge z
$$
即
$$
m\ge \left\lceil\dfrac{z}{k}\right\rceil
$$
除此以外,还需要满足什么要求?
情况一:m 是偶数
由于 $mk-z$ 是偶数,如果 $m$ 是偶数,那么 $z$ 也必须是偶数。
$s$ 中的每个位置至多翻转 $m$ 次。但是,对于 $s$ 中的 $0$,由于要翻转奇数次,所以至多翻转 $m-1$ 次。
所以 $s$ 中的所有位置的翻转次数的上界是 $z(m-1)+(n-z)m$,其可能等于 $mk$,也可能比 $mk$ 大(因为是上界),所以有
$$
z(m-1)+(n-z)m\ge mk
$$
解得
$$
m\ge \left\lceil\dfrac{z}{n-k}\right\rceil
$$
与
$$
m\ge \left\lceil\dfrac{z}{k}\right\rceil
$$
联立得
$$
m\ge \max\left(\left\lceil\dfrac{z}{k}\right\rceil,\left\lceil\dfrac{z}{n-k}\right\rceil\right)
$$
情况二:m 是奇数
由于 $mk-z$ 是偶数,如果 $m$ 是奇数,那么 $z$ 和 $k$ 必须同为奇数,或者同为偶数(奇偶性相同)。
$s$ 中的每个位置至多翻转 $m$ 次。但是,对于 $s$ 中的 $1$,由于要翻转偶数次,所以至多翻转 $m-1$ 次。
所以 $s$ 中的所有位置的翻转次数的上界是 $zm+(n-z)(m-1)$,其可能等于 $mk$,也可能比 $mk$ 大(因为是上界),所以有
$$
zm+(n-z)(m-1)\ge mk
$$
解得
$$
m\ge \left\lceil\dfrac{n-z}{n-k}\right\rceil
$$
与
$$
m\ge \left\lceil\dfrac{z}{k}\right\rceil
$$
联立得
$$
m\ge \max\left(\left\lceil\dfrac{z}{k}\right\rceil,\left\lceil\dfrac{n-z}{n-k}\right\rceil\right)
$$
情况一和情况二取最小值。
如果两个情况都不满足要求,返回 $-1$。
下界可以取到
这可以用 Gale-Ryser 定理证明。
具体来说,我们需要判断是否存在一个 $m$ 行 $n$ 列的 $0\text{-}1$ 矩阵 $M$,第 $i$ 行对应着第 $i$ 次操作,其中 $M_{i,j} = 0$ 表示没有翻转 $s_j$,$M_{i,j} = 1$ 表示翻转 $s_j$。每一行的元素和都是 $k$,第 $j$ 列的元素和是 $s_j$ 的翻转次数 $a_j$。由于 $a_j\le m$ 且 $\sum\limits_{j} a_j\le mk$,由 Gale-Ryser 定理可得,这样的矩阵是存在的。
特殊情况
如果 $z=0$,那么无需操作,答案是 $0$。
由于下界公式中的分母 $n-k$ 不能为 $0$,我们需要特判 $n=k$ 的情况,此时每次操作只能翻转整个 $s$。
- 如果 $z=n$,即 $s$ 全为 $0$,那么只需操作 $1$ 次。
- 否则无论怎么操作,$s$ 中始终有 $0$,返回 $-1$。
上取整转成下取整
关于上取整的计算,当 $a$ 为非负整数,$b$ 为正整数时,有恒等式
$$
\left\lceil\dfrac{a}{b}\right\rceil = \left\lfloor\dfrac{a+b-1}{b}\right\rfloor
$$
证明见 上取整下取整转换公式的证明。
###py
class Solution:
def minOperations(self, s: str, k: int) -> int:
n = len(s)
z = s.count('0')
if z == 0:
return 0
if k == n:
return 1 if z == n else -1
ans = inf
# 情况一:操作次数 m 是偶数
if z % 2 == 0: # z 必须是偶数
m = max((z + k - 1) // k, (z + n - k - 1) // (n - k)) # 下界
ans = m + m % 2 # 把 m 往上调整为偶数
# 情况二:操作次数 m 是奇数
if z % 2 == k % 2: # z 和 k 的奇偶性必须相同
m = max((z + k - 1) // k, (n - z + n - k - 1) // (n - k)) # 下界
ans = min(ans, m | 1) # 把 m 往上调整为奇数
return ans if ans < inf else -1
###java
class Solution {
public int minOperations(String s, int k) {
int n = s.length();
int z = 0;
for (int i = 0; i < n; i++) {
if (s.charAt(i) == '0') {
z++;
}
}
if (z == 0) {
return 0;
}
if (k == n) {
return z == n ? 1 : -1;
}
int ans = Integer.MAX_VALUE;
// 情况一:操作次数 m 是偶数
if (z % 2 == 0) { // z 必须是偶数
int m = Math.max((z + k - 1) / k, (z + n - k - 1) / (n - k)); // 下界
ans = m + m % 2; // 把 m 往上调整为偶数
}
// 情况二:操作次数 m 是奇数
if (z % 2 == k % 2) { // z 和 k 的奇偶性必须相同
int m = Math.max((z + k - 1) / k, (n - z + n - k - 1) / (n - k)); // 下界
ans = Math.min(ans, m | 1); // 把 m 往上调整为奇数
}
return ans < Integer.MAX_VALUE ? ans : -1;
}
}
###cpp
class Solution {
public:
int minOperations(string s, int k) {
int n = s.size();
int z = ranges::count(s, '0');
if (z == 0) {
return 0;
}
if (k == n) {
return z == n ? 1 : -1;
}
int ans = INT_MAX;
// 情况一:操作次数 m 是偶数
if (z % 2 == 0) { // z 必须是偶数
int m = max((z + k - 1) / k, (z + n - k - 1) / (n - k)); // 下界
ans = m + m % 2; // 把 m 往上调整为偶数
}
// 情况二:操作次数 m 是奇数
if (z % 2 == k % 2) { // z 和 k 的奇偶性必须相同
int m = max((z + k - 1) / k, (n - z + n - k - 1) / (n - k)); // 下界
ans = min(ans, m | 1); // 把 m 往上调整为奇数
}
return ans < INT_MAX ? ans : -1;
}
};
###go
func minOperations(s string, k int) int {
n := len(s)
z := strings.Count(s, "0")
if z == 0 {
return 0
}
if k == n {
if z == n {
return 1
}
return -1
}
ans := math.MaxInt
// 情况一:操作次数 m 是偶数
if z%2 == 0 { // z 必须是偶数
m := max((z+k-1)/k, (z+n-k-1)/(n-k)) // 下界
ans = m + m%2 // 把 m 往上调整为偶数
}
// 情况二:操作次数 m 是奇数
if z%2 == k%2 { // z 和 k 的奇偶性必须相同
m := max((z+k-1)/k, (n-z+n-k-1)/(n-k)) // 下界
ans = min(ans, m|1) // 把 m 往上调整为奇数
}
if ans < math.MaxInt {
return ans
}
return -1
}
复杂度分析
- 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $s$ 的长度。瓶颈在遍历 $s$ 上。如果已知 $s$ 中的 $0$ 的个数,则时间复杂度是 $\mathcal{O}(1)$。
- 空间复杂度:$\mathcal{O}(1)$。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
数学 - 区间扩展
Problem: 3666. 使二进制字符串全为 1 的最少操作次数
[TOC]
思路
公式推导
先计数,假设有a个0,则b = n - a,假设选取ta个0进行反转,ta范围推导如下:
$$
0 \le ta \le a \
ta \le k \
k - ta \le b \
k-b \le ta \
\Downarrow \
\max(0,k - b) \le ta \le \min(a,k)
$$
又因为 tb = k - ta,即选取tb个1进行反转
因此a可以扩展为[mn_a,mx_a]:
$$
a + tb - ta = a + k - ta * 2 \
mn_a = a + k - mx_ta * 2 \
mx_a = a + k - mn_ta * 2
$$
# 扩展区间
def getExArea(a):
b = n - a
# max(0,k - b) <= ta <= min(a,k)
mn_ta = max(0,k - b)
mx_ta = min(a,k)
# a + k - ta - ta
mn_a = a + k - mx_ta * 2
mx_a = a + k - mn_ta * 2
return mn_a,mx_a
上面是第一次扩展,那下一次扩展 $a ∈ [mn_a,mx_a] $ 呢?遍历区间内的值肯定超时。
其实,对于每一个a值,就是一个滑动窗口的过程,得到的区间是具有单调性的,因此我们只需要关注区间的边界值即可,因此每一次都对区间边界值进行扩展,得到一个更大的区间即可
区间扩展出口
当满足以下两个条件即可终止扩展:
- $k ∈ [mn_a,mx_a] $
- $ k\ &\ 1 = mn_a\ &\ 1 $
第一个条件很容易理解,a可以满足等于k,那下一次操作直接把k个0转化成1就好了。
第二个条件就是奇偶性相等,为何要满足这个条件呢?其实模拟一下操作就知道了,假设a = 9, b = 11, k = 5
| ta | tb | a 变为 |
|---|---|---|
| 5 | 0 | 4 |
| 4 | 1 | 6 |
| 3 | 2 | 8 |
| 2 | 3 | 10 |
| 1 | 4 | 12 |
| 0 | 5 | 14 |
虽然扩展后,k ∈ [4,14] ,但很明显这个区间范围内的值是公差为2的等差数列,得不到a = k = 5,因此要满足与k奇偶性相等才行。
那如何判断满足不了题意呢?扩展区间过程中,如果还没达到扩展出口条件,且出现扩展区间重复,那就满足不了题意了,return -1
# 获取 0 的个数
a = 0
for w in s:
if w == '0':
a += 1
res = 0
la,ra = a,a
dt = set()
dt.add((la,ra))
while la:
if la <= k <= ra and la&1 == k&1:
res += 1
break
mn_la,mx_la = getExArea(la)
mn_ra,mx_ra = getExArea(ra)
nla = min(mn_la,mn_ra)
nra = max(mx_la,mx_ra)
# 扩展过了
if (nla,nra) in dt:
return -1
la,ra = nla,nra
dt.add((la,ra))
res += 1
return res
贪心优化
- 如果
a >= 2 * k,可以贪心的a -= k,且操作结果+1 - 如果
b >= 2 * k,可以贪心的b -= k
n = len(s)
# 获取 0 的个数
a = 0
for w in s:
if w == '0':
a += 1
b = n - a
# 贪心优化
res = 0
while a >= 2 * k:
res += 1
a -= k
b += k
while b >= 2 * k:
b -= k
n = a + b
更多题目模板总结,请参考2024年度总结与题目分享
Code
###Python3
class Solution:
def minOperations(self, s: str, k: int) -> int:
'''
计数:
a,b 分别为 0,1的数目
0 的数目区间扩展 区间扩展
若 k 为偶数
(a,b)
左扩展
ta <= a
ta <= k
k - ta <= b
max(0,k - b) <= ta <= min(a,k)
tb = k - ta
a = a + tb - ta
a = a + k - ta - ta
b = n - a
求出 a,b 对应最小值,最大值
'''
n = len(s)
# 获取 0 的个数
a = 0
for w in s:
if w == '0':
a += 1
b = n - a
# 贪心优化
res = 0
while a >= 2 * k:
res += 1
a -= k
b += k
while b >= 2 * k:
b -= k
n = a + b
# 扩展区间
def getExArea(a):
b = n - a
# max(0,k - b) <= ta <= min(a,k)
mn_ta = max(0,k - b)
mx_ta = min(a,k)
# a + k - ta - ta
mn_a = a + k - mx_ta * 2
mx_a = a + k - mn_ta * 2
return mn_a,mx_a
la,ra = a,a
dt = set()
dt.add((la,ra))
while la:
if la <= k <= ra and la&1 == k&1:
res += 1
break
mn_la,mx_la = getExArea(la)
mn_ra,mx_ra = getExArea(ra)
nla = min(mn_la,mn_ra)
nra = max(mx_la,mx_ra)
# 扩展过了
if (nla,nra) in dt:
return -1
la,ra = nla,nra
dt.add((la,ra))
res += 1
return res