AGC020 C~F【杂题】

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\),求满足下列条件的字符串:

  1. 长度为 \(a+b\),由 \(a\) 个字符 A\(b\) 个字符 B 构成。
  2. 在此基础上,连续的相同字符个数的最大值最小。
  3. 在此基础上,字典序最小。

输出这个字符串的第 \(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 尽量靠后,因此最优方案形如:

\[\underbrace{ \tt{AA} \cdots \tt{A} }_{L}\tt{B} \ \underbrace{\tt{AA} \cdots \tt{A}}_{L} \tt{B} \cdots \tt{AAA} \ | \ \tt{BBB} \cdots \tt{A} \underbrace{\tt{BB} \cdots \tt{B}}_{L} \tt{A} \underbrace{\tt{BB} \cdots \tt{B}}_{L} \]

如果我们能够找到分界点的位置,那么问题就解决了。考虑先求一个 \(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}\) 个,\(f\) 转移复杂度 \(n\)\(g\) 转移复杂度 \(n \pi(n)\)(其中 \(\pi(n)\) 表示 \(n\) 的约数个数),但实际上远远跑不满,可以通过本题。

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;   
}
posted @ 2022-11-05 17:25  came11ia  阅读(24)  评论(0编辑  收藏  举报