标签: 动态规划

[CSP-S2 2019]划分 题解

[CSP-S2 2019]划分 题解

题目地址:洛谷:P5665 划分 – 洛谷 | 计算机科学教育新生态

题目描述

2048 年,第三十届 CSP 认证的考场上,作为选手的小明打开了第一题。这个题的样例有 $n$ 组数据,数据从 $1 \sim n$ 编号,$i$ 号数据的规模为 $a_i$。

小明对该题设计出了一个暴力程序,对于一组规模为 $u$ 的数据,该程序的运行时间为 $u^2$。然而这个程序运行完一组规模为 $u$ 的数据之后,它将在任何一组规模小于 $u$ 的数据上运行错误。样例中的 $a_i$ 不一定递增,但小明又想在不修改程序的情况下正确运行样例,于是小明决定使用一种非常原始的解决方案:将所有数据划分成若干个数据段,段内数据编号连续,接着将同一段内的数据合并成新数据,其规模等于段内原数据的规模之和,小明将让新数据的规模能够递增。

也就是说,小明需要找到一些分界点 $1 \le k_1 < k_2 < \cdots < k_p < n$,使得:

$$
\sum_{i=1}^{k_1} a_i\le \sum_{i=k_1+1}^{k_2} a_i \le \dots \le \sum_{i=k_p+1}^n a_i
$$

注意 $p$ 可以为 $0$ 且此时 $k_0 = 0$,也就是小明可以将所有数据合并在一起运行。

小明希望他的程序在正确运行样例情况下,运行时间也能尽量小,也就是最小化

$$
\left(\sum_{i=1}^{k_1} a_i \right)^2+\left(\sum_{i=k_1}^{k_2} a_i \right)^2+\cdots +\left(\sum_{i=k_p+1}^n a_i \right)^2
$$

小明觉得这个问题非常有趣,并向你请教:给定 $n$ 和 $a_i$,请你求出最优划分方案下,小明的程序的最小运行时间。

题意简述

给定一个长度为 $n$ 的数列 $\{a_i\}$,求对数列的一个划分 $T = \{ k_1, k_2, \dots, k_p \}$,使得该划分满足 $\sum\limits_{i=1}^{k_1} a_i\le \sum\limits_{i=k_1+1}^{k_2} a_i \le \dots \le \sum\limits_{i=k_p+1}^n a_i$,且最小化 $ \left(\sum\limits_{i=1}^{k_1} a_i \right)^2+\left(\sum\limits_{i=k_1}^{k_2} a_i \right)^2+\cdots +\left(\sum\limits_{i=k_p+1}^n a_i \right)^2 $。

输入输出格式

输入格式:

由于本题的数据范围较大,部分测试点的 $a_i$ 将在程序内生成

第一行两个整数 $n, \text{type}$。$n$ 的意义见题目描述,$\text{type}$ 表示输入方式。

  1. 若 $\text{type} = 0$,则该测试点的 $a_i$ 直接给出。输入文件接下来:第二行 $n$ 个以空格分隔的整数 $a_i$,表示每组数据的规模。
  2. 若 $\text{type} = 1$,则该测试点的 $a_i$ 将特殊生成,生成方式见后文。输入文件接下来:第二行六个以空格分隔的整数 $x, y, z, b_1, b_2, m$。接下来 $m$ 行中,第 $i$($1 \le i \le m$)行包含三个以空格分隔的正整数 $p_i, l_i, r_i$。

对于 $\text{type} = 1$ 的 $23 \sim 25$ 号测试点,$a_i$ 的生成方式如下:

  • 给定整数 $x, y, z, b_1, b_2, m$,以及 $m$ 个三元组 $(p_i, l_i, r_i)$。
  • 保证 $n \ge 2$。若 $n > 2$,则 $\forall 3\le i\le n$,$b_i = (x \times b_{i−1} + y \times b_{i−2} + z) \bmod 2^{30}$。
  • 保证 $1 \le p_i \le n$,$p_m = n$。令 $p_0 = 0$,则 $p_i$ 还满足 $\forall 0 \le i < m$ 有 $p_i < p_{i+1}$。
  • 对于所有 $1 \le j \le m$,若下标值 $i$($1 \le i \le n$)满足 $p_{j−1} < i \le p_j$,则有

$$
a_i=\left( b_i \bmod (r_j-l_j+1) \right) +l_j
$$

上述数据生成方式仅是为了减少输入量大小,标准算法不依赖于该生成方式

输出格式:

输出一行一个整数,表示答案。

输入输出样例

样例 #1

输入样例#1:

5 0
5 1 7 9 9

输出样例#1:

247

样例 #2

输入样例#2:

10 0
5 6 7 7 4 6 2 13 19 9

输出样例#2:

1256

样例 #3

输入样例#3:

10000000 1
123 456 789 12345 6789 3
2000000 123456789 987654321
7000000 234567891 876543219
10000000 456789123 567891234

输出样例#3:

4972194419293431240859891640

数据范围

测试点编号$n\le$$a_i\le$$\text{type}=$
$1\sim 3$$10$$10$$0$
$4\sim 6$$50$$10^3$$0$
$7\sim 9$$400$$10^4$$0$
$10\sim 16$$5\times 10^3$$10^5$$0$
$17\sim 22$$5\times 10^5$$10^6$$0$
$23\sim 25$$4\times 10^7$$10^9$$1$

对于 $\text{type} = 0$ 的测试点,保证答案不超过 $4\times 10^{18}$。

所有测试点满足:$\text{type} \in {0, 1} , 2 \le n \le 4 \times 10^7 , 1 \le a_i \le 10^9 , 1 \le m \le 10^5 ,1 \le l_i \le r_i \le 10^9 , 0 \le x, y, z, b_1, b_2 < 2^{30}$。

题解

参考资料:

这个题的部分思路并不难,但最优解法确实具有一定的难度和复杂程度,以至于从开始写到整理思路写这篇题解一共用了三天时间。

最初的 DP

首先可以设计出一个比较简单的 DP,状态 $f(i, j)$ 表示对前 $i$ 个数字分段,上一段的结尾为 $j$ 的时候段平方和的最小值。转移时,枚举上一段的结尾 $j$,再枚举上上一段的结尾 $k$,先判断是否满足转移条件 $\sum\limits_{t=k+1}^j a_t \leq \sum\limits_{t’=j+1}^i a_{t’}$,然后进行最小化转移:

$$ f(i, j) = \min_{ \sum\limits_{t=k+1}^j a_t \leq \sum\limits_{t’=j+1}^i a_{t’} } \left\{ f(j, k) + \left( \sum\limits_{t=j+1}^i a_i \right)^2 \right\} $$

显然这个 DP 的复杂度为 $O(n^3)$。

贪心策略与 DP 优化

看这个式子:

$$ a^2 + b^2 < (a+b)^2 \ \ \ (a, b > 0) $$

在转移时,最后一段最长肯定越可能满足转移条件。当段较长的时候,可能将段从中间拆成多段也可以满足转移条件。假设切为两段后满足转移条件且前一段之和为 $a$、后一段之和为 $b$,上面的式子告诉我们,这里切成两段后的段平方和更小。因此,段肯定是越短越好的,或者也可以说,最后一段越短越好。事实上,段越短有利于之后分的段也较短。

因此,这里有贪心策略:段分得越短越好。

考虑将贪心应用进 DP 的过程中,既然最后一段越短越好,最优解中的最后一段一定是最短的,因此上一段的结尾不再有多种可能性,而是直接选择使最后一段最短的那一个。将前 $i$ 个数字分段的最优解中,上一段的结尾记为 $lst(i)$,DP 状态也可改为 $f(i)$ 代表前 $i$ 个数字分段的最优解的段平方和。转移方程如下:

$$ f(i) = \min_{ \sum\limits_{t=lst(j)+1}^j a_t \leq \sum\limits_{t’=j+1}^i a_{t’} } \left\{ f(j) + \left( \sum\limits_{t=j+1}^i a_i \right)^2 \right\} $$

复杂度降为 $O(n^2)$,但依然不够。

单调队列

记 $a_i$ 的前缀和为 $s(i)$,将转移条件写成前缀和的形式:

$$ s(j)-s[lst(j)-1] \leq s(i)-s(j) $$

移项可得

$$ s(i) \geq 2s(j)-s[lst(j)-1] $$

上式的左边只与 $i$ 有关,右边只与 $j$ 有关,我们需要找到满足上述条件的最大的 $j$。

此外,还可以发现,当一个 $j$ 满足上述条件时,根据前缀和的单增性可以得到 $s(i+1) \geq s(i) \geq 2s(j)-s[lst(j)-1]$,即 $j$ 也可以作为 $i+1$ 的决策。因此可行的决策点是一个以 $1$ 为左端点的区间,当 $i$ 增大时,决策点的区间右端点也是单增的(性质 1)。显然,$2s(j)-s[lst(j)-1]$ 的值越小,$j$ 越可能成为合法的决策点,也就是说,记 $A(j) = 2s(j)-s[lst(j)-1]$,当 $j_1 > j_2$ 且 $A(j_1) \leq A(j_2)$ 时,$j_1$ 一定优于 $j_2$(性质 2)。

上述性质决定了我们可以应用单调队列优化 DP 转移的复杂度:维护一个 $A(j)$ 数值单调递增且 $j$ 也单增的单调队列,每次转移时找到队列中最大的满足上述条件的决策点 $j_m$(性质 1),转移后仅保留 $j_m$ 而弹出小于 $j_m$ 的满足条件的决策点(性质 2)。转移完成后,计算 $A(i)$ 再将 $i$ 放入队列中,作为以后的状态的可能决策点。

利用单调队列优化 DP 后,转移的复杂度变为 $O(1)$,则总复杂度为 $O(n)$,可以通过极限数据。

卡常

由于最开始的写法使用了大量 STL 和常数略大的写法,在各种 OJ 上都跑不过极限数据,于是开始了卡常。

  • 首先自然是用了 fread 版快读,这算是我的一个习惯。
  • 在高精计算过程中利用特殊条件判断减少了总运算次数,尤其是减少了取模的次数。
  • 原本是在输入之后单独算一遍前缀和,改成了边输入边算前缀和。
  • 由于有点卡空间,复用了前缀和数组 s 的空间作为生成数据用的临时空间。
  • 单调队列没封装,直接把实现展开到 DP 的过程中了。
  • 手动开了个 O3。

最后终于在洛谷上 AC 了。不过没卡的这么极端的时候也能在 LOJ、UOJ 之类的地方跑过,可能洛谷跑的会略有点慢。

严谨性说明和性质证明

本题解的说明并不严谨,在说明单调队列优化转移时并未给出相关性质的证明,表述也不尽清晰。本文受本人水平、时间、精力等所限,暂时无法呈现出更好的效果,欢迎提出建议或改写本文部分内容。

关于有关性质的证明,可以在毛爷爷的博客:CSP2019划分的简要题解 – 博客 – matthew99的博客阅读。

代码

#pragma GCC optimize(3) // 手动开 O3
// Code by KSkun, 2019/12
#include <cstdio>
#include <cctype>
#include <cstring>

#include <algorithm>
#include <utility>

typedef unsigned long long LL;
typedef std::pair<int, int> PII;

inline char fgc() {
	static char buf[100000], * p1 = buf, * p2 = buf;
	return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
		? EOF : *p1++;
}

inline LL readint() {
	register LL res = 0, neg = 1; register char c = fgc();
	for (; !isdigit(c); c = fgc()) if (c == '-') neg = -1;
	for (; isdigit(c); c = fgc()) res = res * 10 + c - '0';
	return res * neg;
}

inline char readsingle() {
	char c;
	while (!isgraph(c = fgc())) {}
	return c;
}

const int MAXN = 40000005, MAXM = 100005;;
const LL MASK = (1ull << 30) - 1; // % 2^30 和 & MARK 等价

int n, type, lst[MAXN];
LL s[MAXN];

void gen_data() {
	LL x, y, z, m;
	x = readint(); y = readint(); z = readint();
	s[1] = readint(); s[2] = readint();
	m = readint();
	int* p = new int[MAXM], * l = new int[MAXM], * r = new int[MAXM];
	p[0] = l[0] = r[0] = 0;
	for (int i = 1; i <= m; i++) {
		p[i] = readint();
		l[i] = readint();
		r[i] = readint();
	}
	for (int i = 3; i <= n; i++) {
		s[i] = (x * s[i - 1] + y * s[i - 2] + z) & MASK; // 复用 s 数组省空间
	}
	for (int i = 1; i <= m; i++) {
		for (int j = p[i - 1] + 1; j <= p[i]; j++) {
			s[j] = (s[j] % (r[i] - l[i] + 1)) + l[i] + s[j - 1]; // 复用 s 数组省空间
		}
	}
	delete[] p; delete[] l; delete[] r; // 内存回收
}

const int BN_MAX = 1e9;
struct BigNum {
	LL dat[5];
	int len;
	BigNum() {
		dat[0] = dat[1] = dat[2] = dat[3] = dat[4] = 0; // 手动初始化没用 memset 减小常数
		len = 0;
	}
	BigNum(LL x) {
		dat[0] = dat[1] = dat[2] = dat[3] = dat[4] = 0;
		len = 0;
		while (x > 0) {
			dat[len++] = x % BN_MAX;
			x /= BN_MAX;
		}
	}

	BigNum operator+(const BigNum& rhs) const {
		BigNum res;
		if (len == 0) return rhs; // 减少运算次数
		if (rhs.len == 0) return *this;

		res.len = std::max(len, rhs.len);
		for (register int i = 0; i < res.len; i++) {
			if (dat[i] == 0 && rhs.dat[i] == 0) continue; // 减少运算次数
			res.dat[i] += dat[i] + rhs.dat[i];
			if (res.dat[i] > BN_MAX) { // 减少运算次数
				res.dat[i + 1] += res.dat[i] / BN_MAX;
				res.dat[i] %= BN_MAX;
			}
		}
		if (res.dat[res.len] != 0) res.len++;
		return res;
	}
	BigNum operator*(const BigNum& rhs) const {
		BigNum res;
		if (len == 0 || rhs.len == 0) return res;

		res.len = len + rhs.len - 1;
		for (register int i = 0; i < len; i++) {
			if (dat[i] == 0) continue; // 减少运算次数
			for (register int j = 0; j < rhs.len; j++) {
				if (rhs.dat[j] == 0) continue; // 减少运算次数
				res.dat[i + j] += dat[i] * rhs.dat[j];
			}
		}
		for (register int i = 0; i < res.len; i++) {
			if (res.dat[i] > BN_MAX) { // 减少运算次数
				res.dat[i + 1] += res.dat[i] / BN_MAX;
				res.dat[i] %= BN_MAX;
			}
		}
		if (res.dat[res.len] != 0) res.len++;
		return res;
	}
};

int d_dat[MAXN], d_l, d_r;

inline LL A(int idx) {
	return 2 * s[idx] - s[lst[idx]];
}

int main() {
	n = readint(); type = readint();

	if (type == 0) {
		for (register int i = 1; i <= n; i++) {
			s[i] = s[i - 1] + readint(); // 输入时就把前缀和做了,减小单独计算的常数
		}
	} else {
		gen_data();
	}

	d_l = d_r = 0; // 单调队列手写而且没封装,常数优化
	for (register int i = 1; i <= n; i++) {
		int lst_ele = 0;
		while (d_l != d_r && s[i] >= A(d_dat[d_l])) {
			lst_ele = d_dat[d_l]; d_l++;
		}
		lst[i] = lst_ele; d_dat[--d_l] = lst_ele;
		LL Ai = A(i); // 没空间存 A 数组,所以用函数,这里是避免重复运算减小常数
		while (d_l != d_r && Ai <= A(d_dat[d_r - 1])) d_r--;
		d_dat[d_r++] = i;
	}

	BigNum ans;
	for (register int i = n; i != 0; i = lst[i]) {
		BigNum t = BigNum(s[i] - s[lst[i]]);
		ans = ans + t * t;
	}
	for (register int i = ans.len - 1; i >= 0; i--) {
		if (i == ans.len - 1) {
			printf("%llu", ans.dat[i]);
		} else {
			printf("%09llu", ans.dat[i]);
		}
	}

	return 0;
}
[CSP-S2 2019]括号树 题解

[CSP-S2 2019]括号树 题解

题目地址:洛谷:P5658 括号树 – 洛谷 | 计算机科学教育新生态

题目背景

本题中合法括号串的定义如下:

  1. () 是合法括号串;
  2. 如果 A 是合法括号串,则 (A) 是合法括号串。
  3. 如果 A,B 是合法括号串,则 AB 是合法括号串。

本题中子串不同的子串的定义如下:

  1. 字符串 $S$ 的子串是 $S$ 中连续的任意个字符组成的字符串。$S$ 的子串可用起始位置 $l$ 与终止位置 $r$ 来表示,记为 $S (l, r)$($1 \le l \le r \le |S|$,$|S|$ 表示 $S$ 的长度)。
  2. $S$ 的两个子串视作不同当且仅当它们在 $S$ 中的位置不同,即 $l$ 不同或 $r$ 不同。

题目描述

一个大小为 $n$ 的树包含 $n$ 个结点和 $n-1$ 条边,每条边连接两个结点,且任意两个结点间有且仅有一条简单路径互相可达。

小 Q 是一个充满好奇心的小朋友,有一天他在上学的路上碰见了一个大小为 $n$ 的树,树上结点从 $1 \sim n$ 编号,$1$ 号结点为树的根。除 $1$ 号结点外,每个结点有一个父亲结点,$u$($2 \le u \le n$)号结点的父亲为 $f_u$($1 \le f_u < u$)号结点。

小 Q 发现这个树的每个结点上恰有一个括号,可能是 ( 或 )。小 Q 定义 $s_i$ 为:将根结点到 $i$ 号结点的简单路径上的括号,按结点经过顺序依次排列组成的字符串。

显然 $s_i$ 是个括号串,但不一定是合法括号串,因此现在小 Q 想对所有的 $i$($1 \le i \le n$)求出,$s_i$ 中有多少个互不相同的子串合法括号串

这个问题难倒了小 Q,他只好向你求助。设 $s_i$ 共有 $k_i$ 个不同子串是合法括号串,你只需要告诉小 Q 所有 $i \times k_i$ 的异或和,即:

$$(1\times k_1)\ \text{xor}\ (2\times k_2)\ \text{xor}\ (3\times k_3)\ \text{xor}\ \cdots \ \text{xor}\ (n\times k_n)$$

其中 $\text{xor}$ 是位异或运算。

题意简述

一棵 $n$ 个节点的树上每个节点有一个左括号或右括号,求出从树根 1 到每个节点的树链对应的括号序列中不同的合法子串个数。

输入输出格式

输入格式:

从文件 brackets.in 中读入数据。

第一行一个整数 $n$,表示树的大小。

第二行一个长为 $n$ 的由 ( 与 ) 组成的括号串,第 $i$ 个括号表示 $i$ 号结点上的括号。

第三行包含 $n−1$ 个整数,第 $i$($1 \le i < n$)个整数表示 $i + 1$ 号结点的父亲编号 $f_{i+1}$。

输出格式:

输出到文件 brackets.out 中。

仅一行一个整数表示答案。

输入输出样例

样例 #1

输入样例#1:

5
(()()
1 1 2 2 

输出样例#1:

6

数据范围

测试点编号$n \leq$特殊性质
$1 \sim 2$$8$$f_i=i-1$
$3 \sim 4$$200$ $f_i=i-1$
$5 \sim 7$$2000$ $f_i=i-1$
$8 \sim 10$ $2000$
$11 \sim 14$$10^5$ $f_i=i-1$
$15 \sim 16$ $10^5$
$17 \sim 20$$5 \times 10^5$

题解

首先考虑如何求出一个括号序列中不同的合法子串数目,这是一个比较经典的括号序列类 DP 问题,它的解法是这样的:

由于括号序列具有 $A$ 合法,则 $(A)$ 合法的可扩展性,因此可以使用栈来完成 DP 的转移。定义 DP 状态 $f(i)$ 表示括号序列 $S$ 中,以 $S_i$ 结尾的合法子串个数。如果 $S_i$ 是左括号,则将 $i$ 放入一个栈中;如果 $S_i$ 是右括号,则向前找到最近的未匹配的左括号位置 $j$ ,这样就可以完成一次匹配,即是 (A) 的状况,因此有以下转移

$$ f(i) = f(j-1)+1 $$

其中,多出来的 $1$ 个合法子串即是 (A) 这一个。向上找未匹配的过程可以用栈实现,在刚才左括号插入的栈中找到栈顶即是最近的未匹配左括号,随后由于该括号已匹配,因此将其弹出栈。

当这个问题上了树,仍然可以用类似的方法解决。同样地定义 $f(u)$ 为从 $1$ 到 $u$ 这条树链对应的括号序列中,以 $S_u$ 结尾的合法子串个数。在树链上向上找到最深的未匹配左括号结点即可。在 DFS 遍历树上节点的过程中,维护这个栈的信息即可。这一部分的细节可以参考代码中的注释。

由于状态定义仅为 $u$ 结尾的合法子串个数, $u$ 节点的答案应是从 $1$ 到 $u$ 的树链上所有节点的 DP 值之和,因此还要做一次 DFS 完成求和。

由于答案可能很大,因此在计算 $u \times f(u)$ 时需要转型成 long long 计算。

代码

UPD:更换了 unsigned long long ,可以通过官方数据。

// Code by KSkun, 2019/11
#include <cstdio>
#include <cctype>

#include <algorithm>
#include <vector>
#include <stack>
#include <utility>

typedef unsigned long long LL;
typedef std::pair<int, int> PII;

inline char fgc() {
	static char buf[100000], * p1 = buf, * p2 = buf;
	return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
		? EOF : *p1++;
}

inline LL readint() {
	LL res = 0, neg = 1; char c = fgc();
	for(; !isdigit(c); c = fgc()) if(c == '-') neg = -1;
	for(; isdigit(c); c = fgc()) res = res * 10 + c - '0';
	return res * neg;
}

inline char readsingle() {
	char c;
	while(!isgraph(c = fgc())) {}
	return c;
}

const int MAXN = 500005;

int n, fa[MAXN];
LL f[MAXN];
char str[MAXN];

std::vector<int> gra[MAXN];
std::stack<int> sta;

void dfs1(int u) {
	int t = -1;
	if (str[u] == ')' && !sta.empty()) {
		t = sta.top();
		sta.pop();
		f[u] += f[fa[t]] + 1;
	} else if (str[u] == '(') {
		sta.push(u);
	}
	for (int v : gra[u]) {
		dfs1(v);
	}
	if (str[u] == ')' && t != -1) sta.push(t);
	else if (str[u] == '(') sta.pop();
}

void dfs2(int u) {
	f[u] += f[fa[u]];
	for (int v : gra[u]) {
		dfs2(v);
	}
}

int main() {
	scanf("%d", &n);
	scanf("%s", str + 1);
	for (int i = 2; i <= n; i++) {
		scanf("%d", &fa[i]);
		gra[fa[i]].push_back(i);
	}
	dfs1(1);
	dfs2(1);
	LL ans = 0;
	for (int i = 1; i <= n; i++) {
		ans ^= i * f[i];
	}
	printf("%llu", ans);
	return 0;
}

[CF6D]Lizards and Basements 2 题解

[CF6D]Lizards and Basements 2 题解

题目地址:Codeforces:Problem – 6D – Codeforces、洛谷:CF6D Lizards and Basements 2 – 洛谷 | 计算机科学教育新生态

题目描述

This is simplified version of the problem used on the original contest. The original problem seems to have too difiicult solution. The constraints for input data have been reduced.

Polycarp likes to play computer role-playing game «Lizards and Basements». At the moment he is playing it as a magician. At one of the last levels he has to fight the line of archers. The only spell with which he can damage them is a fire ball. If Polycarp hits the i-th archer with his fire ball (they are numbered from left to right), the archer loses a health points. At the same time the spell damages the archers adjacent to the i-th (if any) — they lose b (1 ≤ b < a ≤ 10) health points each.

As the extreme archers (i.e. archers numbered 1 and n) are very far, the fire ball cannot reach them. Polycarp can hit any other archer with his fire ball.

The amount of health points for each archer is known. An archer will be killed when this amount is less than 0. What is the minimum amount of spells Polycarp can use to kill all the enemies?

Polycarp can throw his fire ball into an archer if the latter is already killed.

题意简述

有一排共$n$个怪物从左到右依次编号,你可以攻击编号为$2 \sim n-1$的怪物,一次攻击会对攻击怪物造成$a$点伤害,对该怪物两边的怪物造成$b$点伤害,已知每个怪物的生命值,怪物生命值小于0时视为已击败,求击败所有怪物的最少攻击次数及方案。

输入输出格式

输入格式:
The first line of the input contains three integers n, a, b (3 ≤ n ≤ 10; 1 ≤ b < a ≤ 10). The second line contains a sequence of n integers — h1, h2, …, hn (1 ≤ hi ≤ 15), where hi is the amount of health points the i-th archer has.

输出格式:
In the first line print t — the required minimum amount of fire balls.
In the second line print t numbers — indexes of the archers that Polycarp should hit to kill all the archers in t shots. All these numbers should be between 2 and n - 1. Separate numbers with spaces. If there are several solutions, output any of them. Print numbers in any order.

输入输出样例

输入样例#1:

3 2 1
2 2 2

输出样例#1:

3
2 2 2 

输入样例#2:

4 3 1
1 4 1 1

输出样例#2:

4
2 2 3 3 

题解

容易发现的性质是,一个位置上的伤害值只取决于本身、左边和右边三个位置上的攻击次数。考虑动态规划解决本题,令$f(i, j, k)$表示考虑到怪物$i$,且怪物$i$被攻击了$k$次、怪物$i-1$被攻击了$j$次,把$[1, i-1]$内的怪物全部击败的攻击最少次数,由于一定要击败怪物$i$,枚举攻击怪物$i$的次数转移即可,即
$$ f(i, j, k) = \min_{bm+aj+bk>h_{i-1}}\{ f(i-1, m, j) + k \} $$

由于我们不能攻击$n$怪物,这个DP只能最多做到第一维为$n-1$的位置,剩下的需要我们自行判断是否增加几次攻击把怪物$n$击败。只需要在0、击败怪物$n-1$的最少次数、击败怪物$n$的最少次数里取一个max就可以了。

代码在不断的改动中写的比较丑,就凑合着看吧。复杂度$O(nk^3)$,其中$k$指每个位置攻击次数的可能最大值,例如代码中取了$200$。

代码

// Code by KSkun, 2019/8
#include <cstdio>
#include <cctype>
#include <cstring>

#include <algorithm>

typedef long long LL;

inline char fgc() {
	static char buf[100000], * p1 = buf, * p2 = buf;
	return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
		? EOF : *p1++;
}

inline LL readint() {
	LL res = 0, neg = 1; char c = fgc();
	for(; !isdigit(c); c = fgc()) if(c == '-') neg = -1;
	for(; isdigit(c); c = fgc()) res = res * 10 + c - '0';
	return res * neg;
}

inline char readsingle() {
	char c;
	while(!isgraph(c = fgc())) {}
	return c;
}

const int MAXN = 20;

int n, a, b, h[MAXN], f[MAXN][205][205], prei[MAXN][205][205];

int main() {
	n = readint(); a = readint(); b = readint();
	for(int i = 1; i <= n; i++) {
		h[i] = readint() + 1;
	}
	memset(f, 0x3f, sizeof(f));
	for(int i = 0; i <= 200; i++) {
		if(i * b >= h[1]) f[2][0][i] = i;
	}
	for(int i = 3; i < n; i++) {
		for(int j = 0; j <= 200; j++) {
			for(int k = 0; k <= 200; k++) {
				for(int ii = 0; ii <= 200; ii++) {
					if(ii * b + j * a + k * b >= h[i - 1] && f[i][j][k] > f[i - 1][ii][j] + k) {
						f[i][j][k] = f[i - 1][ii][j] + k;
						prei[i][j][k] = ii;
					}
				}
			}
		}
	}
	int ans = 2e9, ai, aj;
	for(int i = 0; i <= 200; i++) {
		for(int j = 0; j <= 200; j++) {
			int res = f[n - 1][i][j] +
				std::max({0, (int)ceil(double(h[n - 1] - i * b - j * a) / a), (int)ceil(double(h[n] - j * b) / b)});
			if(ans > res) {
				ans = res; ai = i; aj = j;
			}
		}
	}
	printf("%d\n", ans);
	for(int i = 1; i <= ans - f[n - 1][ai][aj]; i++) {
		printf("%d ", n - 1);
	}
	int ni = ai, nj = aj, nn = n - 1;
	while(nn != 1) {
		for(int i = 1; i <= nj; i++) printf("%d ", nn);
		int onj = nj; nj = ni; ni = prei[nn][ni][onj]; nn--;
	}
	return 0;
}