2024年11月

平时用IntelliJ IDEA写代码的时候,你有没有用过这些快捷方式:

输入
main
,会弹出自动补全完整的
main
结构:

输入
sout
,会弹出自动补全完整的
System.out
语句:

那么问题来了:

  1. 还有哪些快捷方式?
  2. 如何定义自己想要的?

初识 Live Templates

该功能来自于IntelliJ IDEA的Live Templates配置,你可以通过菜单进入
Setting
,然后搜索
Live Templates
找到它:

点开
Java
就能看预定义的模板了:

不是很多,可以挑你常用的记一下即可。

如果要定义要用的模板,可以点击上面的
+

选择
Live Tempalte
之后在下面会看到一个编辑框:

根据自己需要填写要创建的快捷模板内容。最后记得保存,就可以成功创建了。

尝试在编码框内输入上面定义的快捷方式:
ddfor
,就可以用到上面定义的模板代码了:

使用进阶

上面仅介绍了Live Template最基本的使用方式。如果还不能满足你的要求,下面几项提示也许可以帮到你。

使用分组

如果对这个功能的需求比较多,需要定义比较多模板,尤其是做基础架构给大家定规范做工具的话,还可以在创建Live Template的时候使用Group来创建一些独立的组来方便管理。

使用参数

很多时候我们创建模版还会需要一些动态的信息,比如自定义模板注释的时候,需要使用:时间、用户等动态信息。

在Live Template的模板定义中是支持使用参数的,使用
$$
来引用,两个
$
中间放参数名。Live Template提供了一些预定义的参数,同时也支持用户自定义变量。

关于这块使用参数和有哪些预定义参数,读者可以自行查阅官方文档:
Live template variables

导入导出

如果你想使用别人的模板,或者想把自己的模板分享给被人,那么可以使用导入导出功能。

功能位置如下图:

然后选择你要导出导入的配置内容里选择Live Templates即可

好了,今天的分享就到这里,希望内容对您有用
_
,更多关于IDEA的使用技巧可以收藏
《玩转IDEA专栏》

初探富文本之搜索替换算法

在我们的在线文档系统中,除了基础的富文本编辑能力外,搜索替换的算法同样也是重要的功能实现。纯文本搜索替换算法相对来说是比较容易实现的,而在富文本的场景中,由于存在图文混排、嵌入模块如
Mention
等,实现这一功能则相对要复杂得不少。那么本文则以
Quill
实现的富文本编辑器为基础,通过设计索引查找和图层渲染的方式来实现搜索替换。

描述

首先我们先来思考一下纯文本的搜索替换算法,对于纯文本而言我们可以直接通过一些方法来查找对应文本的索引值,然后无论是将其直接替换还是切割再联合的方式,都可以轻松地完成这个功能。而富文本本质上可以看作是携带着格式的纯文本,那么在这种情况下我们依然可以延续着对于纯文本的处理思路,只不过由于存在文本会被切割为多个片段的问题,就需要我们来兼容这个数据层面上的问题。

实际上在前边我们强调了当前的实现是以
Quill
为基础实现的方案,因为本质上搜索替换与数据结构的实现强相关,
quill-delta
是扁平的数据结构,相对来说比较方便我们来处理这个问题。而如果是嵌套的数据结构类型例如
Slate
的实现,我们仍然可以依照这个思路来实现,只不过在这里如果我们将其作为
DEMO
来实现并没有那么直观,嵌套的数据结构类型对于替换的处理上也会复杂很多。

那么在这里我们先来构造
Quill
的编辑器实例,在我们实现的实例中,我们只需要引用
quill.js
的资源,然后指定实例化的
DOM
节点即可初始化编辑器,我们在这里通过配置来注册了样式模块。文中的相关实现可以在
https://github.com/WindRunnerMax/QuillBlocks/blob/master/examples/find-replace.html
中查看。

<div class="editor-container">
    <div id="$editor"></div>
</div>
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
<script>
const editor = new Quill($editor, {
  modules: {
    toolbar: [[{ header: [1, 2, false] }], ["bold", "italic", "underline"], ["code-block"]],
  },
  placeholder: "Enter Text...",
  theme: "snow",
});
</script>


Quill
中是使用
quill-delta
来描述富文本内容的,
quill-delta
是一个扁平的数据结构,除了能描述富文本内容之外,还可以描述富文本内容的变更。在这里我们首先关注的是文档内容的描述,因此这里我们则是全部以
insert
的形式来描述,而之前我们也提到了富文本实际上就是纯文本携带属性,因此我们可以很轻松地使用下面的样式描述普通文本与加粗文本。

const delta = new Delta([
  { insert: "Hello " },
  { insert: "World", attributes: { bold: true } },
]);

我们的搜索替换方案本质上是在数据描述的基础上来查找目标内容,而从上边的数据结构来看,我们可以明显地看出在
attributes
上也可能存在需要查找的目标内容,例如我们需要实现
Mention
组件,是可以在
attributes
中存储
user
信息来展示
@user
的信息,而
insert
置入的内容则是空占位,那么在这里我们就实现一下
Quill

Mention
插件。

const Embed = Quill.import("blots/embed");
class Mention extends Embed {
  static blotName = "mention";
  static className = "ql-mention";
  static tagName = "SPAN";
  static create(value) {
    const node = super.create(value);
    node.innerHTML = `@${value}`;
    node.setAttribute("data-value", value);
    return node;
  }
  static value(domNode) {
    return domNode.getAttribute("data-value");
  }
}
Quill.register(Mention);

则此时我们的预置的数据结构内容则如下所示,我们可能注意到数据结构中
Mention

insert
是对象并且实际内容是并不是在
attributes
而是在
insert
中。这是因为在
Quill
中描述
parchment

blots/embed

value
是由
insert
存储的
blotName
对象来完成的,因此在示例中我们查找的目标还是在
insert
对象中,但是并不实际影响我们需要对其进行额外的查找实现。

editor.setContents([
  { "insert": "查找替换" },
  { "attributes": { "header": 1 }, "insert": "\n" },
  { "insert": "查找替换的富文本基本能力,除了基本的" },
  { "attributes": { "bold": true }, "insert": "文本查找" },
  { "insert": "之外,还有内嵌结构例如" },
  { "attributes": { "italic": true }, "insert": "Mention" },
  { "insert": "的" },
  { "insert": { mention: "查找能力" } },
  { "insert": "。\n" },
]);

搜索算法

首先我们先来设计一下内容的搜索算法,既然前边我们已经提到了富文本实际上就是带属性的纯文本,那么我们就先来设想对于纯文本的搜索实现。文本搜索算法实现本质上就是字符串文本的匹配,那么我们很容易想到的就是
KMP
算法,而实际上
KMP
算法并不是最高效的实现算法,在各种文本编辑器中实现的查找功能更多的是使用的
Boyer-Moore
算法。

在这里我们先不考虑高效的算法实现,我们实现的目标则是在长文本中查找目标文本的索引值,因此可以直接使用
String
对象的
indexOf
来实现查找。而我们实际上是需要获取目标文本的所有索引值,因此需要借助这个方法的第二个参数
position
来实现查找的起始位置。而如果我们使用正则来实现索引则容易出现输入的字符串被处理为正则保留字的问题,例如搜索
(text)
字符串时,则在不特殊处理的情况下会被认为搜索
text
文本。

const origin = "123123000123";
const target = "123";
const indices = [];
let startIndex = 0;

while ((startIndex = origin.indexOf(target, startIndex)) > -1) {
  indices.push(startIndex);
  startIndex += target.length;
}

console.log(indices); // [0, 3, 9]
const origin = "123123000123";
const target = "123";
const indices = [];
const regex = new RegExp(target, "g");
let match;

while ((match = regex.exec(origin)) !== null) {
  indices.push(match.index);
}

console.log(indices); // [0, 3, 9]

在我们上述的测试中,可以将其放置于
https://perf.link/
测试一下执行性能,将其分别实现防止于
Test Cases
中测试,可以发现实际上
indexOf
的性能还会更高一些,因此在这里我们就直接使用
indexOf
来实现我们的搜索算法。在浏览器中测试万字文本的
indexOf
搜索,结果稳定在
1ms
以内,性能表现是完全可以接受的,当然这个搜索结果和电脑本身的性能也是强相关的。

indexOf // 141,400 ops/s
RegExp // 126,030 ops/s

那么此时我们就来处理关于
Delta
的索引建立,那么由于富文本的表达存在样式信息,反映在数据结构上则是存在文本的片段,因此我们的搜索首先需要直接将所有文本拼接为长字符串,然后使用
indexOf
进行查找。此外在前边我们已经看到数据结构中存在
insert
,在这里我们需要将其作为不可搜索的文本占位,以避免在搜索时将其作为目标文本的一部分。

const delta = editor.getContents();
const str = delta.ops
  .map(op => (typeof op.insert === "string" ? op.insert : String.fromCharCode(0)))
  .join("");
let index = str.indexOf(text, 0);
const ranges = [];
while (index >= 0) {
  ranges.push({ index, length: text.length });
  index = str.indexOf(text, index + text.length);
}

而对于类似于
Mention
模块的
Void/Embed
结构处理则需要我们特殊处理,在这里不像是先前对于纯文本的内容搜索,我们不能将这部分的相关描述直接放置于
insert
中,因此此时我们将难以将其还原到原本的数据结构中,如果不能还原则无法将其显示为搜索的结果样式。那么我们就需要对
delta.ops
再次进行迭代,然后
case by case
地将需要处理的属性进行处理。

const rects = [];
let iterated = 0;
delta.ops.forEach(op => {
  if (typeof op.insert === "string") {
    iterated = iterated + op.insert.length;
    return void 0;
  }
  iterated = iterated + 1;
  if (op.insert.mention) {
    const value = op.insert.mention;
    const mentionIndex = value.indexOf(text, 0);
    if (mentionIndex === -1) return void 0;
    // 绘制节点位置 ...
  }
});

当我们将所有的数据收集到起来后,就需要构建虚拟图层来展示搜索的结果。首先我们需要处理最初处理的
insert
纯文本的搜索结果,由于我们在输入的时候通常都是使用
input
来输入搜索的文本目标,因此我们是不需要处理
\n
的情况,这里的图层处理是比较方便的。而具体的虚拟图层实现在先前的
diff
算法文章中已经描述得比较清晰,这里我们只实现相关的调用。

const rangeDOM = document.createElement("div");
rangeDOM.className = "ql-range virtual-layer";
$editor.appendChild(rangeDOM);
const COLOR = "rgba(0, 180, 42, 0.3)";
const renderVirtualLayer = (startRect, endRect) => {
  // ...
};
const buildRangeLayer = ranges => {
  rangeDOM.innerText = "";
  ranges.forEach(range => {
    const startRect = editor.getBounds(range.index, 0);
    const endRect = editor.getBounds(range.index + range.length, 0);
    rangeDOM.appendChild(renderVirtualLayer(startRect, endRect));
  });
};

而在这里我们更需要关注的是
Mention
的处理,因为在这里我们不能够使用
editor.getBounds
来获取其相关的
BoundingClientRect
,因此这里我们需要自行计算其位置。因此我们此时需要取得
mentionIndex
所在的节点,然后通过
createRange
构建
Range
对象,然后基于该
Range
获取
ClientRect
,这样就取得了我们需要的
startRect

endRect
,但是需要注意的是,此时我们取得是绝对位置,还需要将其换算为编辑器实例的相对位置才可以。

const [leaf, offset] = editor.getLeaf(iterated);
if (
  leaf &&
  leaf.domNode &&
  leaf.domNode.childNodes[1] &&
  leaf.domNode.childNodes[1].firstChild
) {
  const textNode = leaf.domNode.childNodes[1].firstChild;
  const startRange = document.createRange();
  startRange.setStart(textNode, mentionIndex + 1);
  startRange.setEnd(textNode, mentionIndex + 1);
  const startRect = startRange.getBoundingClientRect();
  const endRange = document.createRange();
  endRange.setStart(textNode, mentionIndex + 1 + text.length);
  endRange.setEnd(textNode, mentionIndex + 1 + text.length);
  const endRect = endRange.getBoundingClientRect();
  rects.push({ startRect, endRect });
}

rects.forEach(it => {
  const editorRect = editor.container.getBoundingClientRect();
  const startRect = {
    bottom: it.startRect.bottom - editorRect.top,
    height: it.startRect.height,
    left: it.startRect.left - editorRect.left,
    right: it.startRect.right - editorRect.left,
    top: it.startRect.top - editorRect.top,
    width: it.startRect.width,
  };
  const endRect = {
    bottom: it.endRect.bottom - editorRect.top,
    height: it.endRect.height,
    left: it.endRect.left - editorRect.left,
    right: it.endRect.right - editorRect.left,
    top: it.endRect.top - editorRect.top,
    width: it.endRect.width,
  };
  const block = renderVirtualLayer(startRect, endRect);
  rangeDOM.appendChild(block);
});

批量替换

替换的实现在
Delta
的结构上会比较简单,在先前我们也提到了
Delta
不仅能够通过
insert
描述文档,还可以借助
retain

delete

insert
方法来描述文档的变更,那么我们需要做的就是在上述构造的
ranges
的基础上,构造目标的变更描述。而由于我们先前构造的
Mention
是不允许进行实质性的替换操作的,所以我们只需要关注原本的
insert
文本内容即可。

这里的实现重点是实现了批量的
changes
构造,首先需要定义
Delta
实例,紧接着的
preIndex
是用来记录上一次执行过后的索引位置,在我们的
ranges
循环中,
retain
是用来移动指针到当前即将处理的原文本内容,然后调用
delete
将其删除,之后的
insert
是替换的目标文本,注意此时的指针位置是在目标文本之后,因此需要将
preIndex
更新为当前处理的索引位置,最后将其应用到编辑器即可。

const batchReplace = () => {
  const text = $input1.value;
  const replace = $input2.value;
  if (!text) return void 0;
  const changes = new Delta();
  let preIndex = 0;
  ranges.forEach(range => {
    changes.retain(range.index - preIndex);
    changes.delete(range.length);
    changes.insert(replace);
    preIndex = range.index + range.length;
  });
  editor.updateContents(changes);
  onFind();
};

每日一题

https://github.com/WindrunnerMax/EveryDay

参考

https://quilljs.com/docs/delta
https://www.npmjs.com/package/parchment
https://www.npmjs.com/package/quill-delta/v/4.2.2
https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createRange
https://www.ruanyifeng.com/blog/2013/05/boyer-moore_string_search_algorithm.html
https://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: Visual Grounding with Multi-modal Conditional Adaptation

创新点


  • 提出了多模态条件适应(
    MMCA
    )方法,该方法从一种新颖的权重更新视角改善了视觉引导模型中视觉编码器的特征提取过程。
  • 将提出的
    MMCA
    应用于主流的视觉引导框架,并提出了灵活的多模态条件变换器和卷积模块,这些模块可以作为即插即用组件轻松应用于其他视觉引导模型。
  • 进行广泛的实验以验证该方法的有效性,在四个具有代表性的数据集上的结果显示出显著的改善,且成本较小。

内容概述


视觉定位旨在将传统的物体检测推广到定位与自由形式文本描述相对应的图像区域,已成为多模态推理中的核心问题。现有的方法通过扩展通用物体检测框架来应对这一任务,使用独立的视觉和文本编码器分别提取视觉和文本特征,然后在多模态解码器中融合这些特征以进行最终预测。

视觉定位通常涉及在同一图像中定位具有不同文本描述的物体,导致现有的方法在这一任务上表现不佳。因为独立的视觉编码器对于相同的图像生成相同的视觉特征,从而限制了检测性能。最近的方法提出了各种语言引导的视觉编码器来解决这个问题,但它们大多仅依赖文本信息,并且需要复杂的设计。


LoRA
在适应不同下游任务的高效性的启发,论文引入了多模态条件适配(
MMCA
),使视觉编码器能够自适应更新权重,专注于与文本相关的区域。具体而言,首先整合来自不同模态的信息以获得多模态嵌入,然后利用一组从多模态嵌入生成的权重系数,来重组权重更新矩阵并将其应用于视觉定位模型的视觉编码器。

MMCA


MMCA
遵循典型的端到端编码器-解码器范式:

  1. 给定一幅图像和一个语言表达作为输入将其输入到编码器部分,以生成相应的特征嵌入。
    1. 在语言分支中,语言主干将经过分词的语言表达作为输入,并提取文本特征
      \(f_t\in \mathbb{R}^{N_t\times C_t}\)
      ,其中
      \(N_t\)
      是语言标记的数量。
    2. 在视觉分支中,
      CNN
      主干首先提取一个二维特征图,然后经过一系列变换器编码器层,生成一个展平的视觉特征序列
      \(f_v\in \mathbb{R}^{N_v\times C_v}\)
    3. 多模态条件适应(
      MMCA
      )模块以层级方式应用于卷积层和变换器层的参数矩阵。该模块同时接受视觉和文本特征作为输入,并动态更新视觉编码器的权重,以实现基于语言的视觉特征提取。
  2. 将视觉和文本特征嵌入连接在一起,并在多模态解码器(视觉-语言变换器)的输入中添加一个可学习的标记 [
    REG
    ],该解码器将来自不同模态的输入标记嵌入对齐的语义空间,并通过自注意力层执行模态内和模态间的推理。
  3. 回归头使用 [
    REG
    ] 标记的输出状态来直接预测被指对象的四维坐标
    \(\hat b = (\hat{x}, \hat{y}, \hat{w}, \hat{h})\)
    。与真实框
    \(b = (x, y, w, h)\)
    的训练损失可以表述为:

\[\begin{equation}
\mathcal L=\mathcal L_{smooth-l1}(\hat b, b)+L_{giou}(\hat b, b)
\end{equation}
\]

条件适应

对于视觉引导任务,论文希望不同的指代表达能够控制视觉编码器的一组权重更新,从而引导编码器的注意力集中在与文本相关的区域。然而,直接生成这样的矩阵带来了两个缺点:(
1
)这需要一个大型参数生成器。(
2
)没有约束的生成器可能在训练中对表达式过拟合,而在测试期间却难以理解表达式。


LoRA
的启发,让网络学习一组权重更新的基矩阵并使用多模态信息重新组织更新矩阵。这使得参数生成器变得轻量,并确保网络的权重在同一空间内更新。

具体而言,先对权重更新矩阵进行分解,并将其重新表述为外积的和,通过
\(B_i, A_i\)
并使用加权和来控制适应的子空间:

\[\begin{equation}
\Delta Wx=BAx={\textstyle \sum_{i=1}^{r}} B_i\otimes A_i
\label{eq3}
\end{equation}
\]

\[\begin{equation}
h=W_0x+\Delta Wx=W_0x+{\textstyle \sum_{i=1}^{r}} w_iB_i\otimes A_i
\label{eq4}
\end{equation}
\]

为了简化并且不引入其他归纳偏差,使用线性回归来生成这一组权重:

\[\begin{equation}
[w_1, w_2, ..., w_r]^T
=W_gE_{mm}+
[b_1, b_2, ..., b_r]^T
\label{eq5}
\end{equation}
\]

其中
\(W_g\in \mathbb{R}^{r\times d}, [b_1, b_2, ..., b_r]^T\)
是参数矩阵,
\(E_{mm}\in \mathbb{R}^{d}\)
是特定层的多模态嵌入,它是由文本特征和从前一层输出的视觉特征生成的。

与迁移学习任务不同,这里并不打算微调一小部分参数以适应特定的下游任务,而是希望视觉编码器能够适应各种表达。因此,所有参数矩阵
\(W_0, B, A, W_g, [b_1, b_2, ..., b_r]^T\)
在训练阶段都是可学习的。

多模态嵌入

仅依赖文本信息来引导视觉编码器可能会在某些应用中限制灵活性,并且性能可能会受到文本信息质量的影响。为了缓解这些问题,采用门控机制来调节文本信息的输入。

给定文本特征
\(F_t\in \mathbb{R}^{N_t\times C_t}\)
和展平的视觉特征
\(F_v\in \mathbb{R}^{HW\times C_v}\)
,使用简单门控机制来融合视觉和文本嵌入:

\[\begin{equation}
E_{t} = W_tF_t, E_{v}=W_vF_v
\end{equation}
\]

\[\begin{equation}
\alpha =\sigma[W^1_g\delta(W^2_g(E_{t}+E_{v}))]
\end{equation}
\]

\[\begin{equation}
E_{mm} = \alpha E_{t} + E_{v}
\end{equation}
\]

最后,融合嵌入
\(E_{mm}\)
被用来生成系数,从而指导视觉编码器的权重更新。

适配视觉定位

基于视觉编码器(卷积层和
Transformer
层),进一步提出了多模态条件
Transformer
和多模态条件卷积,用于将
MMCA
应用于视觉定位中。

  • 多模态条件
    Transformer

视觉主干中的
Transformer
编码器层主要由两种类型的子层组成,即
MHSA

FFN
。通过应用多模态条件适应,
MHSA

FFN
的计算变为:

\[\begin{equation}
\begin{split}
h'=softmax(\frac{(W'_q)XX^T(W'^T_k)} {\sqrt{d_k}})W_vX + X\\
W'_q = W_q + \Delta W_{q}, W'_k = W_k + \Delta W_{k}
\end{split}
\label{eq8}
\end{equation}
\]

\[\begin{equation}
X_{output}=LN(MLP(h')+\Delta W_{m}h'+h')
\label{eq9}
\end{equation}
\]

其中
\(\Delta W_{q}, \Delta W_{k}, \Delta W_{m}\)
是查询、关键和
MLP
块的线性投影的条件权重更新。

  • 多模态条件卷积

为了便于应用多模态条件适应,将卷积权重更新展开为一个
2-D
矩阵并用两个矩阵
\(B\in \mathbb{R}^{c_{in}\times r}, A\in \mathbb{R}^{r\times c_{out}k^2}\)
进行近似,秩为
\(r\)
。于是,卷积块的多模态条件适应可以通过两个连续的卷积层
\(Conv_B\)

\(Conv_A\)
来近似:

\[\begin{equation}
X_{output}=Conv_{k \times k}(X)+Conv_A(W_{mm} \odot Conv_B(X))
\label{eq10}
\end{equation}
\]

其中
\(X\)

\(W_{mm}=[w_1, w_2, ..., w_r]^T\)
分别是来自前一卷积层的视觉特征和从多模态嵌入生成的权重系数。在通道维度上计算系数与
\(Conv_B\)
输出的点积,并将输出输入到
\(Conv_A\)
,这相当于重新组织权重更新。

主要实验




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】

work-life balance.

比赛在这里呢

填算符

下发题解说的神马东西,赛时根本想不到

讲一个赛时想得到的
\(O(n\log 值域)\)
的思路,很好理解

我们处理出二进制下每一位上的 1 的最后一次出现的位置,
将第
\(i\ (i\in[0,60])\)
位上的 1 最后一次出现的位置记作
\(pos_i\)

同时我们设
\(H=n-k-1\)
为总共有的
bitor
的操作数

有以下结论:由于
\(pos_i\)

\(i\)
位上最后一个 1,所以一旦它后面放了一个 与,这一位上就是 0 了;若我们想要这一位为 1,必须至少满足从
\(pos_i\)
到最后的运算符全是
bitor

发现有以下情况:


  • \(n-pos_i>H\)
    ,即
    \(pos_i\)
    之后需要放的运算符的数量比
    bitor
    的总操作数多,也就是说在
    \(pos_i\)
    之后我一定需要放
    bitand
    操作,所以这种情况下这一位一定不对答案有贡献


  • \(n-pos_i<H\)
    ,也就是说我可以从
    \(pos_i\)
    的前一个位置开始到最后全放
    bitor
    操作,那么这样第
    \(i\)
    位上可以是 1,为了使值最大,所以第
    \(i\)
    位上一定要是 1,所以从第
    \(pos_i\)
    位到最后必须全是
    bitor
    操作,
    对于这种情况的
    \(i\)
    我们记为合法位


  • \(n-pos_i=H\)
    ,也就是说从第
    \(pos_i\)
    到最后的运算符可以全是
    bitor
    操作,但
    \(pos_i\)
    的前一位只能是
    bitand
    所以我们
    特判从第 1 个位置到
    \(pos_i\)
    的前一位全放
    bitand
    能不能让到第
    \(pos_i\)
    个数时得到的值第 $\forall $
    \(j 满足 [pos_j=pos_i]\)
    位为 1,若能则该位也为合法位,否则不合法

对于所有合法位的
\(pos\)
取最小值设为
\(end\)
,因为已经保证
\(end\)
到最后的预算符全是
bitor
,此时有一下两种可能,而我们想尽量构成第二种可能:

  1. \(end\)
    的前一位预算符也为
    bitor
    ,这样我们一定能达到答案最大了

    ,想使答案最优直接让从
    \(end-2\)
    开始的
    \(k\)
    个运算符为
    bitor
    就好了

  2. \(end\)
    的前一位在某些情况为
    bitand
    也是可以使答案最大的,所以我们
    判断能不能让
    \(end\)
    的前一位为
    bitand
    同样使答案最大;

    发现可以的条件相当于从第
    \(end-1\)
    个数到最前面用仅剩的
    bitor
    操作得到一个答案,使得这个答案第 $\forall $
    \(i 满足 [pos_i=end]\)
    位为 1,若能满足条件则第
    \(end-1\)
    个操作符为
    bitand

满足条件的判断又和上述的第三个情况判断一致了,相当于以
\(end-1\)
为下界,再做一次求
\(min(合法的\ pos)\)
,实质上是不断的递归。

形式化如下:

所以一个递归
\(dfs(end, H)\)
表示下界为
\(end\)
,还剩
\(H\)

bitor
操作,判断能不能得到我想要的答案:

若不能则直接从第
\(end-2\)
开始的
\(k-res\)
个运算符全为
bitand
就是答案(
\(res\)
为在之前的递归中已经确定的
bitand
的个数)

若能
则第
\(end-1\)
个位置可以为
bitand

,并设
\(end'=min(这一层中合法的\ pos)\)

继续递归
\(dfs(end',H-(end-end'))\)
判断第
\(end'-1\)
个位置能不能为
bitand

code:

#include<bits/stdc++.h>
#define Aqrfre(x, y) freopen(#x ".in", "r", stdin),freopen(#y ".out", "w", stdout)
#define mp make_pair
#define Type ll
#define qr(x) x=read()
typedef __int128 INT;
typedef long long ll;
using namespace std;

inline ll read(){
    char c=getchar(); ll x=0, f=1;
    while(!isdigit(c)) (c=='-'?f=-1:f=1), c=getchar();
    while(isdigit(c)) x=(x<<1)+(x<<3)+(c^48), c=getchar();
    return x*f;
}

const int N = 1e6 + 5; 
const int maxn = 1e8;

int n, k, K; ll a[N], b[N];

int la[62], pre[62][N], zh[62], X;
vector<int>v[N], ans, tem, num;

inline bool check(int pos, int op){ // 判断从第一个运算符到第 pos 个全为 & 能不能使得到的值满足条件
    int now = pos + 1; ll x = 0;
    for(int i : v[now]) x += (1 << i);
    if(~X) x += (1 << X);
    int y = a[1];
    for(int i=2; i<=now; i++)
        y = y & a[i];
    if(y & x == x) return true;
    return false;
}

inline void dfs(int pos, int H){ // 递归函数
    if(pos <= 0 and H <= 0) return;
    int now = pos + 1, end = 2e9;
    bool f = true; X = -1;
    for(int x : tem) v[pre[x][now]].clear(); //为方便更新新的一层的 V ,先清空
    for(int x : tem){
        if(pos - pre[x][now] > H or !pre[x][now]){
            f = false; break;
        }
        else if(pos - pre[x][now] < H) // 合法则更新 end 并加入 V
            end = min(end, pre[x][now] - 1), v[pre[x][now]].emplace_back(x);
        else{
            X = x;
            if(pre[x][now] == 1 or check(pre[x][now] - 1, 1))
                end = min(end, pre[x][now] - 1), v[pre[x][now]].emplace_back(x);
            else f = false;
        }
    }
    if(f) ans.emplace_back(pos), k--; // pos 位可以为 &,加到答案中
    if(!k) return;
    if(f and end >= k){
        tem.clear(); for(int x : v[end+1]) tem.emplace_back(x);
        dfs(end, H-(pos-end-1)); //继续递归判断 end 位可否为 &
    }
    else{
        int cnt = k; // pos 位不可以为 &,则最优方案为从 pos-1 到 pos-cnt 全为 &
        for(int i=pos-cnt; i<pos; i++)
            k--, cout<<i<<" ";
        return;
    }
}

signed main(){ // bitop
    Aqrfre(bitop, bitop);

    qr(n); qr(k); K = k;
    for(int i=1; i<=n; i++){
        qr(a[i]);
        for(int j=0; j<62; j++){
            if(a[i] & (1ll << j)) pre[j][i] = la[j], la[j] = i;
            else pre[j][i] = pre[j-ans.size()][i-1]; // 二进制下第 j 位为 1 在第 i 个数之前一次出现的位置
        }
    }

    if(k == n - 1){
        for(int i=1; i<=k; i++) cout<<i<<" ";
        return 0;
    }

    for(int j=0; j<62; j++) //  V 存当前这一层递归的下界包含的 最后一个 1 出现在这个下界的 二进制位
        if(la[j]) zh[j] = la[j], v[zh[j]].emplace_back(j);

    int H = n - 1 - k, endi = 1e9; bool go = false;
    for(int i=0; i<62; i++){ // 把第一次递归剖出来单独做
        if(!zh[i]) continue; 
        if(n - zh[i] > H) continue; 
        if(n - zh[i] < H){
            endi = min(endi, zh[i] - 1);
            continue;
        }
        if(go) continue;
        if(n - zh[i] == H){
            if(check(zh[i] - 1, 0)){ //特殊的:合法直接输出
                for(int i=1; i<=k; i++)
                    cout<<i<<" ";
                return 0;
            }
            go = true;
        }
    }

    for(int x : v[endi+1]) tem.emplace_back(x); //tem 暂存下界这个数的 V

    H -= (n - endi - 1);

    dfs(endi, H);
    sort(ans.begin(), ans.end());
    
    for(int x : ans) cout<<x<<" ";



    return 0;
}

我们在开发桌面应用的时候,不管是之前C#开发Winform的时候,还是现在使用wxpython来开发跨平台应用的时候,都需要了解布局的处理,wxpython的常用布局Sizer类,包括BoxSizer,FlexGridSizer,GridBagSizer都是我们需要经常打交道的,因此有必要对它们进行一些了解,这样开发界面起来才能得心应手。本篇随笔介绍一下这几种布局Sizer的不同以及对它们进行测试和封装使用。

1、BoxSizer,FlexGridSizer,GridBagSizer的布局介绍和差异

在 wxPython 中,布局管理是通过 Sizer 类来实现的。常用的 Sizer 类包括
BoxSizer

FlexGridSizer

GridBagSizer
。下面是这些 Sizer 的介绍及其之间的差异:

1. BoxSizer

  • 描述
    :
    BoxSizer
    是最简单的 Sizer 类型,允许你将控件沿一个方向(水平或垂直)排列。
  • 用法
    : 适用于简单的线性布局。你可以指定方向(
    wx.HORIZONTAL

    wx.VERTICAL
    )并控制每个控件的边距和比例。
  • 特点
    :
    • 所有子控件按顺序排列。
    • 可以设置比例,控制控件的伸缩行为。
    • 较适合创建简单的、单一方向的布局。

2. FlexGridSizer

  • 描述
    :
    FlexGridSizer
    是一个可以在行和列中进行灵活布局的 Sizer。它会自动调整每个单元格的大小,以适应控件的内容。
  • 用法
    : 适用于需要均匀分配空间的网格布局。
  • 特点
    :
    • 行和列的大小可以根据内容自动调整。
    • 所有控件都放置在独立的单元格中,不能跨越多个单元格。
    • 适合需要规则网格的情况,比如表单或简单的网格布局。

3. GridBagSizer

  • 描述
    :
    GridBagSizer
    是一种更复杂的布局管理器,它允许你在一个网格中放置控件,并支持控件的大小、位置以及跨越多个行和列。
  • 用法
    : 适用于需要高度自定义布局的情况,比如复杂的用户界面。
  • 特点
    :
    • 支持控件在网格中的精确控制。
    • 可以设置控件的对齐方式和边距。
    • 功能强大,适合复杂布局。

总结

  • BoxSizer
    : 最简单,适合线性布局。
  • FlexGridSizer
    : 灵活的网格,适合不规则网格布局。
  • GridBagSizer
    : 功能最强,支持复杂的自定义布局。

选择合适的 Sizer 取决于你的具体布局需求。对于简单场景,使用
BoxSizer
就足够了;而对于更复杂的布局,
FlexGridSizer

GridBagSizer
提供了更多的灵活性和控制能力。

2、使用布局Sizer控件创建不同的界面案例

为了直观的了解集中Sizer布局控件的不同,我们使用几个例子来测试它们的界面效果和区别。

1)BoxSizer的界面及代码

它是使用BoxeSizer来垂直放置几个部分的内容的,其中底部的两个按钮又是创建一个新的Panel进行维护,如下代码所示。

classMyForm(wx.Frame):def __init__(self):
wx.Frame.
__init__(self, None, wx.ID_ANY, title='Boxesizer 测试')#Add a panel so it looks correct on all platforms self.panel =wx.Panel(self, wx.ID_ANY)#使用内置图标 bmp = wx.ArtProvider.GetBitmap(wx.ART_INFORMATION, wx.ART_OTHER, (48, 48))
titleIco
=wx.StaticBitmap(self.panel, wx.ID_ANY, bmp)
title
= wx.StaticText(self.panel, wx.ID_ANY, '测试内容')#设置标题的字体大小 font =title.GetFont()
font.SetPointSize(
28)
title.SetFont(font)
#使用内置图标 bmp = wx.ArtProvider.GetBitmap(wx.ART_TIP, wx.ART_OTHER, (16, 16))
inputOneIco
=wx.StaticBitmap(self.panel, wx.ID_ANY, bmp)
labelOne
= wx.StaticText(self.panel, wx.ID_ANY, 'Input 1')
inputTxtOne
= wx.TextCtrl(self.panel, wx.ID_ANY, '')

inputTwoIco
=wx.StaticBitmap(self.panel, wx.ID_ANY, bmp)
labelTwo
= wx.StaticText(self.panel, wx.ID_ANY, 'Input 2')
inputTxtTwo
= wx.TextCtrl(self.panel, wx.ID_ANY,'')

inputThreeIco
=wx.StaticBitmap(self.panel, wx.ID_ANY, bmp)
labelThree
= wx.StaticText(self.panel, wx.ID_ANY, 'Input 3')
inputTxtThree
= wx.TextCtrl(self.panel, wx.ID_ANY, '')

okBtn
= wx.Button(self.panel, wx.ID_ANY, '确定')
cancelBtn
= wx.Button(self.panel, wx.ID_ANY, '取消')
self.Bind(wx.EVT_BUTTON, self.onOK, okBtn)
self.Bind(wx.EVT_BUTTON, self.onCancel, cancelBtn)

topSizer
=wx.BoxSizer(wx.VERTICAL)
titleSizer
=wx.BoxSizer(wx.HORIZONTAL)
inputOneSizer
=wx.BoxSizer(wx.HORIZONTAL)
inputTwoSizer
=wx.BoxSizer(wx.HORIZONTAL)
inputThreeSizer
=wx.BoxSizer(wx.HORIZONTAL)
btnSizer
=wx.BoxSizer(wx.HORIZONTAL)

titleSizer.Add(titleIco, 0, wx.ALL,
5)
titleSizer.Add(title, 0, wx.ALL,
5)

inputOneSizer.Add(inputOneIco, 0, wx.ALL,
5)
inputOneSizer.Add(labelOne, 0, wx.ALL,
5)

inputOneSizer.Add(inputTxtOne,
1, wx.ALL|wx.EXPAND, 5)

inputTwoSizer.Add(inputTwoIco, 0, wx.ALL,
5)
inputTwoSizer.Add(labelTwo, 0, wx.ALL,
5)
inputTwoSizer.Add(inputTxtTwo,
1, wx.ALL|wx.EXPAND, 5)

inputThreeSizer.Add(inputThreeIco, 0, wx.ALL,
5)
inputThreeSizer.Add(labelThree, 0, wx.ALL,
5)
inputThreeSizer.Add(inputTxtThree,
1, wx.ALL|wx.EXPAND, 5)

btnSizer.Add(okBtn, 0, wx.ALL,
5)
btnSizer.Add(cancelBtn, 0, wx.ALL,
5)

topSizer.Add(titleSizer, 0, wx.CENTER)
topSizer.Add(wx.StaticLine(self.panel,), 0, wx.ALL
|wx.EXPAND, 5)
topSizer.Add(inputOneSizer, 0, wx.ALL
|wx.EXPAND, 5)
topSizer.Add(inputTwoSizer, 0, wx.ALL
|wx.EXPAND, 5)
topSizer.Add(inputThreeSizer, 0, wx.ALL
|wx.EXPAND, 5)
topSizer.Add(wx.StaticLine(self.panel), 0, wx.ALL
|wx.EXPAND, 5)
topSizer.Add(btnSizer, 0, wx.ALL
|wx.ALIGN_RIGHT, 5)

self.panel.SetSizer(topSizer)
topSizer.Fit(self)

上面可以看到,BoxSizer比较僵硬,它只能垂直或者水平放置内容,如果具有水平和垂直方向的处理,就需要分别创建多个不同的BoxSizer进行管理,因此代码会相对多一些。

2)FlexGridSizer的界面及代码

同样使用FlexGridSizer也可以做到很好的控制,通过设置指定行或者列的拉伸效果,可以很好的实现自动拉伸功能,类似Winform里面的Dock的方向处理。

该布局例子的代码如下所示。

classFrame(wx.Frame):def __init__(self):
super().
__init__(None, title = '测试FlexGridSizer', size=(600, 400))#self.SetBackgroundColour("#4f5049") self.SetMinSize((300, 350))

main_sizer
= wx.BoxSizer( wx.VERTICAL ) #主面板布局使用垂直方向 sizer= wx.FlexGridSizer(2, (10,20))
sizer.Add(wx.StaticText(self, label
='昵称'))
sizer.Add(wx.TextCtrl(self), flag
=wx.EXPAND)
sizer.Add(wx.StaticText(self, label
='留言'))
sizer.Add(wx.TextCtrl(self, style
=wx.TE_MULTILINE), flag=wx.EXPAND, proportion=1)

sizer.Add(wx.StaticText(self, label
='测试'))
sizer.Add(wx.TextCtrl(self), flag
=wx.EXPAND)

sizer.Add(wx.StaticText(self, label
='测试其他'))
sizer.Add(wx.TextCtrl(self), flag
=wx.EXPAND)#添加一行,使其占用两列 sizer.AddSpacer(0) #占位符,保持布局 sizer.Add(wx.TextCtrl(self, value='测试占用两列'), flag=wx.EXPAND, proportion=1)

self.sampleList
= ['friends', 'advertising', 'web search', 'Yellow Pages']
self.edithear
= wx.ComboBox(self, size=(95, -1), style=wx.CB_DROPDOWN)
self.Bind(wx.EVT_COMBOBOX, self.EvtComboBox, self.edithear)
self.edithear.AppendItems(self.sampleList)

sizer.Add(wx.StaticText(self, label
='下拉列表', size=(100, -1)))
sizer.Add(self.edithear, flag
=wx.EXPAND)

sizer.AddGrowableRow(
1)
sizer.AddGrowableCol(
1)

main_sizer.Add(sizer, flag
=wx.EXPAND|wx.ALL, proportion=1, border=10)
self.SetSizer(main_sizer)

okBtn
= wx.Button(self, wx.ID_ANY, "确定")
cancelBtn
= wx.Button(self, wx.ID_ANY, '取消')
btnSizer
=wx.BoxSizer(wx.HORIZONTAL)
btnSizer.Add(okBtn, 0, wx.ALL,
5)
btnSizer.Add(cancelBtn, 0, wx.ALL,
5)
main_sizer.Add(btnSizer, flag
=wx.ALIGN_RIGHT|wx.ALL, border=10)

前面我们提到了,该布局控件的
所有控件都放置在独立的单元格中,不能跨越多个单元格

如果您需要实现控件跨越多行或多列的布局,应该使用
wx.GridBagSizer

wx.GridBagSizer
是一个更灵活的布局管理器,允许控件在网格中跨越多个单元格。

3)GridBagSizer的界面及代码

这是一个简单的案例,主要来介绍
GridBagSizer 的缩放效果及其跨行的实现的。

它的代码如下所示

classMyFrame(wx.Frame):def __init__(self):
super().
__init__(None, title="GridBagSizer 示例")#创建一个 BoxSizer 作为外层 sizer outer_sizer =wx.BoxSizer(wx.VERTICAL)#创建一个 GridBagSizer grid_sizer = wx.GridBagSizer(5, 5) #行间距和列间距为 5 #添加控件到 sizer grid_sizer.Add(wx.StaticText(self, label="姓名"), pos=(0, 0), flag=wx.ALIGN_CENTER_VERTICAL, border=10)
grid_sizer.Add(wx.TextCtrl(self, size
= (100, -1)), pos=(0, 1), flag=wx.EXPAND|wx.ALL, border=10)#添加一个控件,占用 1 行 2 列 grid_sizer.Add(wx.StaticText(self, label="占用两列的控件,测试内容很长很长很长很长"), pos=(1, 0), span=(1, 2), flag=wx.EXPAND)#添加更多控件 grid_sizer.Add(wx.StaticText(self, label="介绍内容"), pos=(2, 0), flag=wx.ALIGN_CENTER_VERTICAL, border=10)
grid_sizer.Add(wx.TextCtrl(self, style
=wx.TE_MULTILINE), pos=(2, 1), flag=wx.EXPAND|wx.ALL, border=10)#让控件跟随窗口拉伸 grid_sizer.AddGrowableCol(1) #允许第二列拉伸 grid_sizer.AddGrowableRow(2) #允许第三行拉伸 #将 grid_sizer 添加到 outer_sizer,并设置顶部边距 outer_sizer.Add(grid_sizer, flag=wx.EXPAND | wx.ALL, border=10) #设置顶部、底部、左侧和右侧边距 #设置外层 sizer self.SetSizer(outer_sizer)
self.SetSize((
400, 300)) #设置初始大小 outer_sizer.Fit(self)#调整窗口大小以适应控件 self.Layout()

注意,我们如果要使得控件能够拉伸,通过设置指定布局的行或者列可以拉伸即可,如上面代码介绍。

        #让控件跟随窗口拉伸
        grid_sizer.AddGrowableCol(1)  #允许第二列拉伸
        grid_sizer.AddGrowableRow(2)  #允许第三行拉伸

注意上面的代码中的位置,以及跨行的设置代码

pos=(1, 0), span=(1, 2)


wx.GridBagSizer
中,
pos

span
参数用于控制控件在网格中的位置和跨越的行列数。

1. pos 参数

  • 描述
    :
    pos
    用于指定控件在网格中的位置。它是一个二元组,格式为
    (行索引, 列索引)
  • 示例
    :
    pos=(1, 0)
    表示将控件放置在第 2 行(索引从 0 开始)第 1 列。

2. span 参数

  • 描述
    :
    span
    用于指定控件跨越的行数和列数。它也是一个二元组,格式为
    (行数, 列数)
  • 示例
    :
    span=(1, 2)
    表示该控件在垂直方向上跨越 1 行,在水平方向上跨越 2 列。

结合使用:
当将 pos span 一起使用时,可以创建复杂的布局。例如:

sizer.Add(wx.StaticText(self, label="占用两列的控件"), pos=(1, 0), span=(1, 2), flag=wx.EXPAND)

在这个示例中,控件的位置是
(1, 0)
,意味着它放置在第二行第一列;而
span=(1, 2)
意味着它占用整整 1 行和 2 列的空间。这使得该控件可以在横向上扩展到第二列,形成一个跨越的效果。

wx.GridBagSizer
是 wxPython 中一个非常灵活和强大的布局管理器,适用于需要复杂布局的用户界面。对于动态添加或删除控件的情况,
GridBagSizer
能够很好地处理控件的重新排列和调整。

复杂布局的简化

  • 组合使用
    :
    GridBagSizer
    可以与其他 Sizer(如
    BoxSizer

    FlexGridSizer
    )组合使用,以满足更复杂的布局需求。
  • 层次结构
    : 可以在
    GridBagSizer
    中嵌套其他 Sizer,从而实现多层次的布局管理。

wx.GridBagSizer
提供了强大的功能和灵活性,使得开发者可以创建复杂和响应式的用户界面。通过合理使用
pos

span
、边距设置及可扩展性选项,可以有效提升应用程序的用户体验。

如下面界面效果,就是基于该布局创建的。