WebKit Inside: CSS 样式表的匹配时机介绍了当 HTML 页面有不同 CSS 样式表引入时,CSS 样式表开始匹配的时机。后续文章继续介绍 CSS 样式表的匹配过程,但是在匹配之前,首先需要收集页面里面的 Active 样式表。

1 Active 样式表

在一个 HTML 文件里面,可能会使用<style>标签与<link>标签引入许多样式表,但是这些样式表并不一定都同时在文档里面生效。有时根据业务需求,可能会只使用页面里的部分样式表。比如有一个换肤需求,页面里面可能会使用<link>标签引入 4 张样式表,代码如下:

<link href="reset.css" rel="stylesheet" />

<link href="default.css" rel="stylesheet" title="Default Style" />
<link href="fancy.css" rel="alternate stylesheet" title="Fancy" />
<link href="basic.css" rel="alternate stylesheet" title="Basic" />

上面样式表reset.css所在的<link>标签有rel="stylesheet"属性,没有title属性,这种样式表被称为 Persisten 样式表,会一直被启用。

样式表default.css所在的<link>标签有rel="stylesheet"title属性,这种样式表被称为 Preferred 样式表。Preferred 样式表是默认启用。一个页面只能有一个 Preferred 样式表。

样式表fancy.cssbasic.css所在<link>标签有rel="alternate stylesheet"title属性,这种样式表被称为 Alternate 样式表。这些样式表默认下是不启用的,但是可以提供给用户选择。一旦用户选择了一个 Alternate 样式表,Preferred 样式表就会别禁用。

根据 <link>标签语法,样式表reset.cssdefault.css会在页面里面使用,而样式表fancy.cssbasic.css会暂时不使用。

一般这种场景会给一个按钮让用户切换皮肤,当用户选择切换到Fancy皮肤时,样式表default.css就失效,样式表fancy.css就会启用。但是不管用户如何切换,样式表reset.css始终有效。更多信息可以参考 MDN Alternative Style Sheet[1]

在用户没有换肤之前,样式表reset.cssdefault.css样式表就属于 Active 样式表,当用户选择切换之后,样式表reset.cssfancy.css就是 Active 样式表。

在进行 CSS 样式表匹配之前,WebKit 首先要收集页面里面所有的 Active 样式表,然后依次遍历这些 Active 样式表的 CSS Rule 进行匹配。

2 相关类图



上面类图里Style::SCope类持有负责进行样式表匹配的Style::Resolver类,同时它内部还有 3 个重要的数据成员:

m_styleSheetCandidateNodes是一个哈希链表,用来按顺序存储 HTML 文件里面的 <style><link>节点,也就是 HTMLStyleElment对象和HTMLLinkElement对象。

m_activeStyleSheets是一个 Vector,类似数组,用来顺序存储页面里面的 Active 样式表。

m_styleSheetsForStyleSheetList也是一个 Vector,用来顺序存储页面里面的所有样式表。

3 获取 Candidate Node

无论内部样式表,还是外部样式表,当 WebKit 解析到 <style>标签或者<link>标签时,都会调用Style::Scope::addStyleSheetCandidateNode方法,将自己添加到Style::Scope的实例变量m_styleSheetCandidateNode里面。

以内部样式表为例,下面是调用堆栈:

函数Style::Scope::addStyleSheetCandidateNode的代码如下:

void Scope::addStyleSheetCandidateNode(Node& node, bool createdByParser)
{
if (!node.isConnected())
return; // Until the <body> exists, we have no choice but to compare document positions,
// since styles outside of the body and head continue to be shunted into the head
// (and thus can shift to end up before dynamically added DOM content that is also
// outside the body).
// 1. createByParser 代表当前的 node 是从 HTML 文件里解析出来的,而不是通过 JavaScript 代码动态创建的;
// m_document.bodyOrFrameset 方法判断当前页面是否解析出了 <body> 标签和 <frameset> 标签;
// 如果前这两个条件为真,那么节点直接添加到变量 m_styleSheetCandidateNodes;
// 还有一种情形 m_styleSheetCandidateNodes 当前还没有添加任何节点
if ((createdByParser && m_document.bodyOrFrameset()) || m_styleSheetCandidateNodes.isEmptyIgnoringNullReferences()) {
m_styleSheetCandidateNodes.add(node);
return;
} // Determine an appropriate insertion point.
// 2. 如果上述条件不满足,就会走到这里,这里会将当前节点与 m_styleSheetCandidateNodes 里已有的 <style> 或者 <link>
// 节点进行位置比较,以便按照正确的顺序把当前节点插入到 m_styleSheetCandidateNodes.
auto begin = m_styleSheetCandidateNodes.begin();
auto end = m_styleSheetCandidateNodes.end();
auto it = end;
RefPtr<Node> followingNode;
do {
// 3. // 从后向前遍历
--it;
Ref<Node> n = *it;
unsigned short position = n->compareDocumentPosition(node);
// 4. DOCUMENT_POSITION_FOLLOWING 表示当前节点 node 位于节点 n 后面
if (position == Node::DOCUMENT_POSITION_FOLLOWING) {
if (followingNode)
// 5. 注意 followwingNode 位于节点 n 的后面,这里将节点 node 插入到 followwingNode 前面,
// 也就是刚好插儒道节点 n 的后面
m_styleSheetCandidateNodes.insertBefore(*followingNode, node);
else
// 6. 如果节点 node 位于节点 n 后面,但是节点插入之前节点 n 后面已经没有其他节点了,那么就直接
// 将节点 node 添加到节点 n 后面
m_styleSheetCandidateNodes.appendOrMoveToLast(node);
return;
}
followingNode = WTFMove(n);
} while (it != begin); LOG_WITH_STREAM(StyleSheets, stream << "Scope " << this << " addStyleSheetCandidateNode() " << node); // 7. 如果遍历到 m_styleSheetCandidateNodes 最前面,上面代码也没有找到合适的位置,
// 那么就将节点 node 插入到最前面.
m_styleSheetCandidateNodes.insertBefore(*followingNode, node);
}

上面代码注释 1 是向变量m_styleSheetCandidateNodes添加 node 节点的第一处代码。变量createByParser代表当前节点 node 是从 HTML 文件里解析出来的,而不是通过 JavaScript 代码动态创建出来的。函数document.bodyOrFrameset代表当前是否已经解析出了<body>标签后者<frameset>标签。如果满足前面这两个条件,或者当前m_styleSheetCandidateNodes里为空,那么就将当前 node 添加进去。

如果上面条件都不满足,代码会运行到注释 2 处。注释 2 后面的代码会将当前 node 节点与m_styleSheetCandidateNodes变量里已有的<style>后者<link>标签的位置相比较,以便按照正确的位置将当前节点 node 插入到m_styleSheetCandidateNodes

那什么是正确的位置呢?

因为样式表的位置影响着样式表里 CSS Rule 中声明的优先级。比如 HTML 页面通过<link>标签引入了 2 个样式表 A 与 B,其中样式表 B 位于 样式表 A 后面。如果样式表 A 有如下 CSS Rule:

div {
background-color: red;
}

样式表 B 的 CSS Rule 和样式表一样,只是设置背景色为蓝色:

div {
background-color: blue;
}

由于样式表 A 和 B 都是 Author 样式表[2],而且 Specificity[3] 也一样,因此声明的优先级取决于它们所在的位置。由于样式表 B 比样式表 A 更靠后,因此最终会应用样式表 B 中的背景色。

上面代码注释 3 处就是从后向前遍历m_styleSheetCandidateNodes,以便找到这个正确位置。

注释 4 处比较节点n与节点node的位置关系[4]。如果节点node位于节点n的后面,也就是DOCUMENT_POSITION_FOLLOWING,那么就可以插入节点。

DOCUMENT_POSITION_FOLLOWING的意义是按照 DOM 树的 Tree Order[5][6] 进行遍历,节点node位于位于节点n之后。Tree Order 就是按照先序-深度优先(preorder,depth-first)遍历。

假设有如下的 HTML:

<html>
<head>
<link rel="stylesheet" href="./test1.css" />
<link rel="stylesheet" href="./test2.css" />
</head>
<body>
<div>Hello</div>
<p>World</p>
</body>
</html>

其 DOM 树结构如下:



按照 Tree Order 先序-深度优先遍历,那么就是先遍历根节点,然后遍历从左起第一棵子树,然后是第二棵子树,然后是第三棵子树...。

遍历的顺序如上图所示,遍历结果如下:html->head->title->link->link->body->div->p。从遍历结果可以看到,第 2 个 <link>标签位于第 1 个<link>标签后面,也就是第 2 个 <link>标签following 第 1 个 <link>标签。

从遍历结果上看,按照先序-深度优先遍历的位置关系,正好是 HTML 文件里面各标签的书写位置关系。

代码注释 5、6、7 都是将节点node插入到m_styleSheetCandidateNodes合适的位置,也就是说 HTML 里面是按照什么顺序引入的样式表,m_styleSheetCandidateNodes就是按照同样的顺序存储的<style>或者<link>标签节点。

4 获取 Active 样式表

无论内部样式表还是外部样式表,当其解析完成之后,都会调用对应的checkLoaded方法。

内部样式表的调用如下:

void InlineStyleSheetOwner::createSheet(Element& element, const String& text)
{
...
auto contents = StyleSheetContents::create(String(), parserContextForElement(element));
m_sheet = CSSStyleSheet::createInline(contents.get(), element, m_startTextPosition);
...
// 1. 解析内部样式表
contents->parseString(text);
...
// 2. 调用 checkLoaded 方法
contents->checkLoaded();
...
}

上面代码注释 1 解析内部样式表。

代码注释 2 调用checkLoaded方法。

外部样式表的调用如下:

void HTMLLinkElement::setCSSStyleSheet(const String& href, const URL& baseURL, const String& charset, const CachedCSSStyleSheet* cachedStyleSheet)
{
...
auto styleSheet = StyleSheetContents::create(href, parserContext);
initializeStyleSheet(styleSheet.copyRef(), *cachedStyleSheet, MediaQueryParserContext(document())); // FIXME: Set the visibility option based on m_sheet being clean or not.
// Best approach might be to set it on the style sheet content itself or its context parser otherwise.
// 1. 解析外部样式表
if (!styleSheet.get().parseAuthorStyleSheet(cachedStyleSheet, &document().securityOrigin())) {
...
}
...
// 2. 调用 checkLoaded 方法
styleSheet.get().checkLoaded();
...
}

上面代码注释 1 解析外部样式表。

注释 2 解析调用checkLoaded方法。

这两种情形的 checkLoaded方法最终会调用到Scope::didChangeActiveStyleSheetCandidates方法,代码如下:

void Scope::didChangeActiveStyleSheetCandidates()
{
scheduleUpdate(UpdateType::ActiveSet);
}

Scope::didChangeActiveStyleSheetCandidates方法内部只调用了一个方法Scope::scheduleUpdate,传给它的参数是UpdateType::ActiveSet

UpdateType类型是一个枚举,定义在StyleScope.h,其定义如下:

  // 定义在 StyleScope.h
enum class UpdateType : uint8_t {
ActiveSet, // 代表一个样式表解析完成,称为了 Active 样式表
ContentsOrInterpretation
};

枚举UpdateType::ActiveSt代表一个 Active 样式表可用了。

方法 Scope::scheduleUpdate方法如下:

void Scope::scheduleUpdate(UpdateType update)
{
...
if (!m_pendingUpdate || *m_pendingUpdate < update) {
// 1. 这里设置 m_pendingUpdate
m_pendingUpdate = update;
...
}
...
// 2. 启动 Timer,Timer 的回调函数触发 Active 样式表的收集.
// 参数 0 代表不延时,立即触发
m_pendingUpdateTimer.startOneShot(0_s);
}

上面代码注释 1 设置Style::Scope对象的一个变量m_pendingUpdate,这个变量在后续触发 Active 样式表收集使用。

代码注释 2 启用一个 Timer,Timer 的回调函数触发 Active 样式表的收集流程。

Timer 的回调函数如下:

void Scope::pendingUpdateTimerFired()
{
/// 1. 触发 Active 样式表收集
flushPendingUpdate();
}

上面代码注释 1 调用Style::Scope::flushPendingUpdate方法触发 Active 样式表收集。

方法Style::Scope::flushPendingUpdate代码如下:

inline void Scope::flushPendingUpdate()
{
...
// 1. m_pendingUpdate 已经在方法 Style::Scope::scheduleUpdate 里设置
if (m_pendingUpdate)
flushPendingSelfUpdate();
}

上面代码注释 1 处变量m_pendingUpdate已经在方法Style::Scope::scheduleUpdate里面设置成了UpdateType::ActiveSset,所以这里直接调用方法Style::Scope::flushPendingSelfUpdate方法。

Style::Scope::flushPendingSelfUpdate方法代码如下:

void Scope::flushPendingSelfUpdate()
{
ASSERT(m_pendingUpdate);
auto updateType = *m_pendingUpdate;
// 1. 清除 m_pendingUpdate 变量,给其置空
clearPendingUpdate();
// 2. 收集 Active 样式表
updateActiveStyleSheets(updateType);
}

上面代码注释 1 首先清除变量m_pendingUpdate,给其置空。

代码注释 2 调用Style::Scope::updateActiveStyleShhets方法开始真正的收集 Active 样式表。

方法Style::Scope::updateActiveStyleSheets代码如下:

void Scope::updateActiveStyleSheets(UpdateType updateType)
{
...
// 1. 收集 Active 样式表.
// collection 变量里面存储着收集到的 Active 样式表和页面里面所有样式表.
auto collection = collectActiveStyleSheets();
// 2. 变量 activeCSSStyleSheets 里面存储收集到的 Active 样式表,collection 变量里的 Active 样式表会赋值给这个变量
Vector<RefPtr<CSSStyleSheet>> activeCSSStyleSheets;
// 3. 上面已经收集了最新添加的 Active 样式表,这里进行过滤,剔除那些比如样式表长度为 0 的样式表,
// 过滤后的结果存储在 activeCSSStyleSheets 中.
filterEnabledNonemptyCSSStyleSheets(activeCSSStyleSheets, collection.activeStyleSheets);
...
// 4. 将 Active 样式表存储到 m_activeStyleSheets
m_activeStyleSheets.swap(activeCSSStyleSheets);
// 5. 将所有样式表存储到 m_styleSheetsForStyleSheetList
m_styleSheetsForStyleSheetList.swap(collection.styleSheetsForStyleSheetList);
...
}

上面代码注释 1 调用方法Style::Scope::collectActiveStyleSheet收集页面里面的 Active 样式表和所有样式表,将结果存储在变量collection中。

代码注释 2 声明的变量activeCSSStyleSheets会存储变量collection中的 Active 样式表。

代码注释 3 现将变量collection中的 Active 样式表进行过滤,剔除那些比如样式表长度为 0 的样式表,过滤后的结果存储在activeCSSStyleSheets变量中。

代码注释 4 将变量activeCSStyleSheet的值交换给实例变量m_activeStyleSheets,也就是m_activeStyleSheets现在存储着页面里面的 Active 样式表。

同理,代码注释 5 将页面里面所有的样式表存储在实例变量m_styleSheetsForStyleSheetList里。

下面看一下 Active 样式表的收集过程,也就是函数Style::Scope::collectActiveStyleSheet,代码如下:

auto Scope::collectActiveStyleSheets() -> ActiveStyleSheetCollection
{
...
// 1. 存储 Active 样式表
Vector<RefPtr<StyleSheet>> sheets;
// 2. 存储 HTML 页面里面所有样式表
Vector<RefPtr<StyleSheet>> styleSheetsForStyleSheetsList; // 3. 遍历之前存储在 m_styleSheetCandidateNodes 里的 <style> 或者 <link> 标签节点对象
for (auto& node : m_styleSheetCandidateNodes) {
RefPtr<StyleSheet> sheet;
if (is<ProcessingInstruction>(node)) {
// 4. ProcessingInstruction 就是诸如 <?xml> 这样的标签
... } else if (is<HTMLLinkElement>(node) || is<HTMLStyleElement>(node) || is<SVGStyleElement>(node)) {
Element& element = downcast<Element>(node);
...
// Get the current preferred styleset. This is the
// set of sheets that will be enabled.
if (is<SVGStyleElement>(element))
sheet = downcast<SVGStyleElement>(element).sheet();
else if (is<HTMLLinkElement>(element))
// 5. 获取外部样式表
sheet = downcast<HTMLLinkElement>(element).sheet();
else
// 6. 获取内部样式表
sheet = downcast<HTMLStyleElement>(element).sheet(); if (sheet)
// 7. 将样式表添加到 styleSheetsForStyleSheetsList
styleSheetsForStyleSheetsList.append(sheet); // Check to see if this sheet belongs to a styleset
// (thus making it PREFERRED or ALTERNATE rather than
// PERSISTENT).
auto& rel = element.attributeWithoutSynchronization(relAttr);
if (!enabledViaScript && sheet && !title.isEmpty()) {
...
// 8. 如果 <link> 标签的 rel 属性包含 alternate,并且有 title,这里将 sheet 设置为 null,
// 后面也添加不到 Active 样式表了.
if (title != m_preferredStylesheetSetName)
sheet = nullptr;
}
// 9. 如果 <link> 标签的 rel 属性包含了 alternate,并且没有 title 属性,那么也将 sheet 设置为 null,
// 后面也添加不到 Active 样式表了.
if (rel.contains("alternate"_s) && title.isEmpty())
sheet = nullptr;
...
}
if (sheet)
// 10. 将当前样式表添加到 Active 样式表中
sheets.append(WTFMove(sheet));
}
...
// 11. 将结果返回
// sheets 存储 Active 样式表
// styleSheetsForStyleSheetsList 存储所有样式表
return { WTFMove(sheets), WTFMove(styleSheetsForStyleSheetsList) };
}

上面代码注释 1 声明变量sheets用来存储页面里面的 Active 样式表。

代码注释 2 声明变量styleSheetsForStyleSheetsList用来存储页面里面的所有样式表。

代码注释 3 遍历之前存储在m_styleSheetCandidateNodes实例变量里面的<style>标签和<link>标签节点对象。

代码注释 4 处理Processing Instruct[7],不在收集 Active 样式表考虑之内。

代码注释 5 和代码注释 6 根据遍历的节点对象,从其上面获取到对应的样式表对象sheet

代码注释 7 将上面获取到的注释表对象存储到变量styleSheetsForStyleSheetsList,这样styleSheetsForStyleSheetsList里面就是存储的是页面里面所有的样式表。

代码注释 8 处理 Alternate 样式表,也就是<link>标签的rel属性包含alternate,并且title属性有值,此时代码将变量sheet设置为null,这样后续这张样式表就添加不到 Active 样式表里面了。

代码注释 9 同样也是处理 Alternate 样式表,使其后续无法添加到 Active 样式表里面。

代码注释 10 将获取到的样式表添加到sheets变量,也就是 Active 样式表中。

代码注释 11 将收集的 Active 样式表和页面里面所有样式表返回出去。

5 小结

要获取 HTML 样式表里的 Active 样式表,首先就要获取页面里面的<style>标签节点对象和<link>标签节点对象。这些节点对象在<style>标签和<link>标签插入到 DOM 树时按照 TreeOrder 顺序存储在Style::Scope的实例变量m_styleSheetCandidateNodes中。

然后,当内部样式表或者外部样式表解析成功之后,会触发 Active 样式表的收集,收集过程就是遍历Style::Scope的实例变量m_styleSheetCandidateNodes,将<style>标签节点或者<link>标签节点关联的样式表收集到Style::Scope的实例变量m_activeStyleSheets中。


  1. MDN Alternative Style Sheet

  2. MDN Introducing the CSS Cascade

  3. MDN Specificity

  4. MDN compareDocumentPosition

  5. https://dom.spec.whatwg.org/#concept-tree-order

  6. https://dom.spec.whatwg.org/#dom-node-document_position_following

  7. MDN ProcessingInstruction

WebKit Insie: Active 样式表的更多相关文章

  1. 深度理解CSS样式表,内有彩蛋....

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  2. CSS的样式表基本概念

    一.样式表分类 1.内联样式表 <p style="fint-size:24px;">直接在标签内部进行样式设置</style> 2.内嵌样式表 <h ...

  3. CSS样式----图文详解:css样式表和选择器

    主要内容 CSS概述 CSS和HTML结合的三种方式:行内样式表.内嵌样式表.外部样式表 CSS四种基本选择器:标签选择器.类选择器.ID选择器.通用选择器 CSS三种扩展选择器:组合选择器.后代选择 ...

  4. CSS样式表 选择器

    1.内联样式表 和HTML联合显示,控制精确,但是可重用性差,冗余较多. 例:<p style="font-size:14px;">内联样式表</p> &l ...

  5. html-css样式表

    一.CSS:Cascading Style Sheet—层叠样式表,其作用是美化HTML网页. 样式表分类:内联样式表.内嵌样式表.外部样式表 1.内联样式表 和HTML联合显示,控制精确,但是可重用 ...

  6. css 样式表

    CSS(cascading style sheets,层叠样式表),作用是美化HTML网页. /*注释*/   注释语法 2.1 样式表的基本概念 2.1.1样式表的分类 1.内联样式表 和HTML联 ...

  7. css样式表:样式分类,选择器。样式属性,格式与布局

    样式表分类: 1.内联样式表, 和html联合显示,例:<p style="font-size:14px;">内联样式表</p> 2.内嵌样式表 作为一个独 ...

  8. HTML第二部分 CSS样式表

    CSS(cascading style sheets,层叠样式表),作用是美化HTML网页. /*注释*/   注释语法 2.1 样式表的基本概念 2.1.1样式表的分类 1.内联样式表 和HTML联 ...

  9. HTML--3css样式表

    CSS(Cascading Style Sheet,叠层样式表),作用是美化HTML网页. /*注释区域*/    此为注释语法 一.样式表 (一)样式表的分类 1.内联样式表 和HTML联合显示,控 ...

  10. 4、网页制作Dreamweaver(样式表CSS)

    样式表style 制作一个风格统一的网页,需要样式表对颜色.字体等属性的规范,同时也省去在body中多次定义的麻烦,所以一个样式表是必不可少的. 样式表有两种引用的方法:一种是直接写在html的< ...

随机推荐

  1. 深入Python网络编程:从基础到实践

    Python,作为一种被广泛使用的高级编程语言,拥有许多优势,其中之一就是它的网络编程能力.Python的强大网络库如socket, requests, urllib, asyncio,等等,让它在网 ...

  2. 使用libavcodec将mp3音频文件解码为pcm音频采样数据【[mp3float @ 0x561c1ec49940] Header missing】

    一.打开和关闭输入文件和输出文件 想要解决上面提到的问题,我们需要对mp3文件的格式有个大致了解,为了方便讲解,我这里画了个示意图: ID3V2 包含了作者,作曲,专辑等信息,长度不固定,扩展了 ID ...

  3. Spring Loaded代码热更新实践和原理分析

    1.引言 开发者在编码效率和快速迭代中的痛点场景包括: 修改代码后,需要频繁重启应用,导致开发效率低下: 实时调试时,不能立即看到代码修改的结果: 大型项目中,重启的时间成本较高. 针对这些问题,本文 ...

  4. MODBUS-TCP转Ethernet IP 网关连接空压机配置案例

    本案例是工业现场应用捷米特JM-EIP-TCP的Ethernet/IP转Modbus-TCP网关连接欧姆龙PLC与空压机的配置案例.使用设备:欧姆龙PLC,捷米特JM-EIP-TCP网关, ETHER ...

  5. selenium实战学习--定位元素

    from selenium import webdriverfrom selenium.webdriver.common.by import Byfrom selenium.common import ...

  6. CentOS 30分钟部署免费在线客服系统

    前段时间我发表了一系列文章,开始介绍基于 .net core 的在线客服系统开发过程.期间有一些朋友希望能够给出 Linux 环境的安装部署指导,本文基于 CentOS 7.9 来安装部署. 我详细列 ...

  7. Hexo博客Yilia主题添加相册功能,丰富博客内容,Next等其他主题可以参考

    实现思路 1.在主页上必须有一个可供点击的相册连接 2.要用 hexo 生成一个photos.html文件 3.photos.html中的图片数据来源?因为这是一个静态页面所有要有一个 json文件 ...

  8. Mybatis(生命周期 )

    生命周期和作用域 生命周期和作用域,是至关重要的,因为错误的使用导致非常严重并发问题 对象声明周期和依赖注入框架 依赖注入框架可以创建线程安全的,基于事务的SqlSession和映射器,并将它们直接注 ...

  9. spring-mvc系列:简介和基本使用

    目录 一.简介 1.什么是MVC 2.什么是SpringMVC 3.SpringMVC的特点 二.基本使用 1.开发环境 2.创建maven工程 3.配置web.xml 4.创建SpringMVC的配 ...

  10. P1941 [NOIP2014 提高组] 飞扬的小鸟 题解

    我们先不管障碍物. 设 \(f[i][j]\) 表示来到点 \((i,j)\) 的最少点击屏幕数. 因为每秒要不上升 \(k\times x[i]\),要么下降 \(y[i]\). 所以有: \[f[ ...