C. Median Sum
给定 \(n\) 个整数组成的集合 \(S = \{a_1,a_2,\cdots,a_n\}\),求 \(S\) 的非空子集和的中位数。
\(n,a_i \leq 2 \times 10^3\),时限 \(\text{2.0s}\)。
记 \(L = \sum a_i\)。重要的观察是,若存在一个和为 \(x\) 的子集,那么必然存在一个和为 \(L - x\) 的子集。也就是说,所有子集和关于 \(\lfloor \frac{L}{2} \rfloor\) 对称。但由于我们不考虑 \(0\),因此中位数恰为 \(\geq \lfloor \frac{L}{2} \rfloor\) 的数中最小的一个。
DP 出 \(f_i\) 表示 \(i\) 这个子集和是否存在即可,利用 bitset 即可优化到 \(O(\frac{nL}{\omega})\)。
code
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 5;
int n, s, a[N]; bitset <N> f;
int main() {
ios :: sync_with_stdio(0);
cin >> n, f[0] = 1;
for (int i = 1; i <= n; i++) cin >> a[i], s += a[i];
for (int i = 1; i <= n; i++) f |= f << a[i];
for (int i = (s + 1) >> 1; i <= s; i++) if (f[i]) {
return printf("%d\n", i), 0;
}
return 0;
}
D. Min Max Repetition
\(q\) 次询问,每次询问给定四个整数 \(a,b,c,d\),求满足下列条件的字符串:
- 长度为 \(a+b\),由 \(a\) 个字符
A
和 \(b\) 个字符B
构成。 - 在此基础上,连续的相同字符个数的最大值最小。
- 在此基础上,字典序最小。
输出这个字符串的第 \(c\) 位到第 \(d\) 位。
\(q \leq 10^3\),\(a_i,b_i \leq 5 \times 10^8\),\(c_i - d_i + 1 \leq 100\),时限 \(\text{2.0s}\)。
记最小的最长连续段的长度为 \(L\),\(m = \min(A,B)\),\(n = \max(A,B)\)。
先考虑如何求出 \(L\)。\(n=m\) 的情况平凡,否则用较少的去隔断较多的,这样至多能分成 \(n+1\) 段,故有 \(L(n+1) \geq m\),由此可得 \(L_{\min} = \lceil \frac{m}{n+1} \rceil\)。
接下来考虑字典序最小这个条件,这等价于所有的 B
尽量靠后,因此最优方案形如:
如果我们能够找到分界点的位置,那么问题就解决了。考虑先求一个 \(x\) 表示 A
在左半边被分成的段数:
\(L = 1\) 的情况平凡,否则考虑分成 \(x\) 段需要 \(x-1\) 个 B
用来切割,于是右半部分剩下 \(r_B = B - x + 1\) 个 B
。而左半部分至少需要 \(L(x-1)+1\) 个 A
,因此右半部分最多剩下 \(r_A = A - L(x-1) - 1\) 个 A
用来切割 B
。由于剩下的 A
必须能够把 B
切割成若干不超过 \(L\) 的段,因此 B
至多能剩下 \((r_A + 1)L\) 个,由此可得 \(r_B \leq (r_A +1 )L\),代入 \(r_A,r_B\) 可得 \(x \leq \frac{L(A+L)-B-1}{L^2-1}\)。
取 \(x_{\max} = \lfloor \frac{L(A+L)-B-1}{L^2-1} \rfloor\) 时字典序最小。由此容易推出右半部分的大小,其中 B
有 \(r_B\) 个,A
有 \(\lfloor \frac{r_B}{L+1} \rfloor\) 个,分界点的位置也就知道了。时间复杂度 \(O(q(D-C))\)。
code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int q, A, B, C, D;
void solve(int A, int B, int C, int D) {
LL l = max((max(A, B) - 1) / (min(A, B) + 1) + 1, 1), x;
if (l != 1) x = max((l * (A + l) - B - 1) / (l * l - 1), 1LL);
else x = (A >= B ? A : 0);
for (int i = C; i <= D; i++) {
if (i <= A + B - (B - x + 1) - (B - x + 1) / l) putchar(i % (l + 1) ? 'A' : 'B');
else putchar((A + B - i + 1) % (l + 1) ? 'B' : 'A');
}
puts("");
}
int main() {
ios :: sync_with_stdio(0);
cin >> q;
for (int i = 1; i <= q; i++) {
cin >> A >> B >> C >> D;
solve(A, B, C, D);
}
return 0;
}
E. Encoding Subsets
定义一个 \(01\) 串的压缩是满足如下方式的字符串变化过程:
- \(0\rightarrow 0,1\rightarrow 1\)。
- 如果 \(A\rightarrow P,B\rightarrow Q\) 合法,那么 \(A+B\rightarrow P+Q\) 也合法(其中 \(+\) 代表字符串拼接)。
- 如果 \(S=\underbrace{A+A+\cdots+A}_{n\text{个}(n\ge 2)}\),那么 \(S\rightarrow(A\times n)\) 也合法(其中
(
,)
,×
为字符,\(n\) 为数字,算作一个字符)。
同时定义 \(01\) 串 \(B\) 是 \(A\) 的子集当且仅当 \(|A|=|B|\),且 \(\forall B_i=1,A_i=1\)。
现在给定一个 \(01\) 串 \(S\),求它所有子集的合法变化结果数之和对 \(998244353\) 取模后的值。
\(|S| \leq 100\),时限 \(\text{5.0s}\)。
首先考虑如果只求给定串的变化方案数怎么做,就是一个简单区间 DP:
\(f_{i,j}\) 代表 \([i,j]\) 方案数,\(g_{i,j}\) 代表 \([i,j]\) 缩成一个括号的方案数。\(f\) 转移时枚举第一个括号对应的区间,\(g\) 转移就枚举区间长度 \(d\) 的约数缩成一个括号。
然后考虑原问题,发现同样可以用类似的方式转移,只不过 \(g\) 转移的时候要从每一段对应的字符串取 AND 后的字符串的 \(f\) 转移,因此 DP 状态里需要完整记录字符串而非一个区间,记搜即可。
这东西虽然看起来很离谱,但是它的复杂度其实是对的。简单分析一下:首先 \(f\) 转移用到的 \(f\) 一定是原来 \(f\) 的子串,因此这部分不用考虑。同时 \(f\) 也会产生 \(g\),而这些 \(g\) 会产生一些 \(f\),这些 \(f\) 是需要重新计算的。
我们构造一颗搜索树,其中每个节点是一个字符串,根节点为 \(S\),每一个儿子都是父亲的一个长为 \(kL(k \geq 2)\) 的子串的 \(k\) 段字符串求 AND 后的结果。对于四层以后的字符串,长度一定不超过 \(\lfloor \frac{n}{8} \rfloor\),这样的字符串至多有 \(2^{\lfloor \frac{n}{8} \rfloor}\) 个,这其实完全是可以接受的。
于是我们只需要统计前三层。如果某一层的 \(k > 2\),那么这个串的长度不会超过 \(\lfloor \frac{n}{6} \rfloor\),类似地这样的串至多有 \(2^{\lfloor \frac{n}{6} \rfloor}\) 个,也是可以接受的,因此我们不妨假定 \(k = 2\)。
对于每一个节点,假设其长度为 \(L_1\),父亲长度为 \(L_2\),那么 \(L_1\) 对应 \(S\) 中 \(4\) 个长为 \(L_1\) 的子串的 AND,并且第 \(1,2\) 个串和第 \(3,4\) 个串在 \(S\) 中分别相邻。因此这个字符串仅取决于第一个起点、第二个起点以及串长,至多有 \(n^3\) 个。
因此不同的状态数至多 \(n^3 + 2^{\lfloor \frac{n}{6}\rfloor}\) 个,转移复杂度 \(n^2\),因此总复杂度 \(O(n^5 + n^2 2^{\lfloor \frac{n}{6}\rfloor})\)。但实际上远远跑不满,可以通过本题。
code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int mod = 998244353;
map <string, LL> mp[2];
string s;
LL f(int ty, string s) {
if (mp[ty].find(s) != mp[ty].end()) return mp[ty][s];
LL ret = 0; int len = s.length();
if (ty) {
for (int i = 1; i <= len; i++) ret = (ret + f(0, s.substr(0, i)) * f(1, s.substr(i, len))) % mod;
return mp[ty][s] = ret;
} else {
for (int i = 1; i < len; i++) if (!(len % i)) {
string t = "";
for (int j = 0; j < i; j++) t += '1';
for (int j = 0; j < len; j += i)
for (int k = 0; k < i; k++) if (s[j + k] == '0') t[k] = '0';
ret = (ret + f(1, t)) % mod;
}
return mp[ty][s] = ret;
}
}
int main() {
ios :: sync_with_stdio(0);
mp[0][""] = mp[1][""] = 1;
mp[0]["0"] = mp[1]["0"] = 1;
mp[0]["1"] = mp[1]["1"] = 2;
cin >> s;
cout << f(1, s) << endl;
return 0;
}
F. Arcs on a Circle
你有一个长度为 \(c\) 的圆,你在上面画了 \(n\) 个弧,第 \(i\) 条弧长为 \(l_i\)。
每一条弧 \(i\) 以如下方式随机均匀地放置在圆上:选择圆上的一个随机实点,然后出现一条以该点为中心的长度为 \(l_i\) 的弧。弧是独立放置的,它们可以相互交叉或包含。
求圆上的每一个实点被至少一条弧覆盖的概率。注意,我们认为一条弧覆盖了它的两个端点。
\(n \leq 6\),\(l_i < c \leq 50\),时限 \(\text{5.0s}\)。
棘手的地方在于实数这个条件,有没有什么办法能够将坐标都转化为整数呢?
注意每条弧的长度都是整数这个条件:考虑两个坐标 \(A,B\),显然以 \(A\) 开始的长度为 \(L\) 的弧能覆盖到 \(B\) 当且仅当 \(\lfloor B\rfloor-\lfloor A\rfloor<L\),或 \(\lfloor B\rfloor-\lfloor A\rfloor=L\) 且 \((B-\lfloor B\rfloor)<(A-\lfloor A\rfloor)\)。这就告诉我们,坐标的小数部分具体是什么并不重要,我们只关心它们的相对大小。
又因为小数部分为取值范围为 \([0,1)\) 的连续性变量,因此出现两点小数部分相同的概率为 \(0\),因此我们可以暴力枚举这 \(n\) 条线段小数部分的相对大小,这一共有 \(n!\) 种情况,它们是等可能的,计算出它们覆盖整个圆周的概率后除以 \(n!\) 即可。
接下来考虑确定这 \(n\) 条线段小数部分的相对大小后怎样计算概率。首先我们固定住一条线段的位置,这样即可断环成链。之后我们可以把圆拆成 \(n \cdot c\) 个点,其中第 \(i \cdot n + j\) 个点表示整数部分为 \(i\),小数部分的相对顺序为 \(j\),然后设 \(f_{i,j,S}\) 表示现在考虑到点 \(i\),当前最远能覆盖到点 \(j\),已经选了的点的集合为 \(S\) 的方案数,转移就考虑当前放不放线段,显然如果放线段那么放置的线段是唯一的,因此时间复杂度 \(O(n! \cdot 2^n \cdot (nc)^2)\)。
最后有一个细节:我们固定了一条线段作为起始,假设我们用一个长度较小的线段作为圆周的起始位置,我们的 DP 过程对于线段超过断成的链的部分会直接忽略,但有可能会出现长度较大的线段从链的末尾开始覆盖,多出的部分又绕回来补上前面空隙的情况,这种情况不会被我们计算,但实际上这种方案也是合法的。因此我们强制令长度最大的线段的起点为圆周的起始位置就行了。
code
#include <bits/stdc++.h>
#define mem(x, v) memset(x, v, sizeof(x))
using namespace std;
const int N = 7, M = 5e2 + 5, S = 1 << N;
int n, c, a[N];
double f[M][S], ans, t;
int main() {
ios :: sync_with_stdio(0);
cin >> n >> c;
for (int i = 1; i <= n; i++) cin >> a[i];
sort(a + 1, a + n + 1);
do {
mem(f, 0), f[a[n] * n][0] = 1;
for (int i = 0; i <= n * c; i++)
for (int j = i; j <= n * c; j++)
for (int S = 0; S < (1 << n - 1); S++)
if (i % n && !((S >> (i % n - 1)) & 1))
f[min(n * c, max(j, i + a[i % n] * n))][S | (1 << (i % n - 1))] += f[j][S];
ans += f[n * c][(1 << n - 1) - 1];
t++;
} while (next_permutation(a + 1, a + n));
printf("%.15lf\n", (double)ans / t / pow(c, n - 1));
return 0;
}