A 开关
考场上一直在那里想消元技巧,然后垫底了。
记一个开关 (i) 按 (j) 步被打开的指数型生成函数:
显然是带标号球的拼接:
记 (f_j=j![x^j]F(x)),有 (f_j) 表示随机操作 (j) 次,开关达到正确状态的概率。
但是题目要求是第一次到达,考虑容斥。
记 (g_j=j![x^j]prod_{i=1}^n {e^{p_ix}+e^{-p_ix}over 2}),表示操作 (j) 次,开关保持不变的概率。
那么可以做一个 dp,
rep(i, 1, n)
rep(j, 0, i - 1)
f[i] -= f[j] * g[i - j];
容易发现求得的 (f_i) 表示操作 (i) 次第一次到达正确状态的概率。
这个 (dp) 是多项式乘法逆的形式,即
有不少题目都可以这么容斥:
比如坐标系中游走 (k) 步,求最后一步是第一次到达 ((x,y)) 的方案数。
设 (f_i) 为 (i) 步的答案,(g_i) 是走 (i) 步最后一步在 ((x,y)) 的方案,(h_i) 表示走 (i) 步原地不动,
很容易枚举第一次访问的时刻,根据容斥进行 (dp) 写出的代码和上面一样。
即有 (f(x)={g(x)over h(x)})。
再比如:坐标系中游走 (k) 步,求经过 (A(x,y)) 恰好一次的方案数。
设 (f_i) 为 (i) 步的答案,(g_i) 是走 (i) 步经过了 ((x,y)) 的方案,(h_i) 表示走 (i) 步原地不动,
考虑进行容斥进行 (dp),可以枚举第一次到 (A) 和最后一次到 (A) 中间间隔的步数,可以发现中间这段方案数是 (h_i),而首尾部分拼起来相当于恰好经过一次 (A) 那么方案显然是 (f_i),那么 (dp) 式子和上题(以及上面的代码)是完全一致的,即 (f(x)={g(x)over h(x)})。
但是这个问题中生成函数 (g(x)) 并不好表示。
一个例题:(m) 维坐标系中游走 (k) 步(每一维 (±1)),求经过本质不同的格点的期望。
考虑对上题所有点的 (dp) 方程叠加到一起:
设 (f_i) 为 (i) 步的所有方案经过的不重复点的个数,(g_i) 是走 (i) 步所有方案经过的所有点,(h_i) 表示走 (i) 步原地不动,
(f(x)={g(x)over h(x)})
(g_i=(2^m)^i(i+1),h_i={ichoose i/2}^m) 都是可以直接得到的,
所以该问题可以直接一遍多项式求逆解决。
回到开关这个题,
那么有概率型生成函数的性质,期望步数即:
(ans=h'(1))
模拟一下多项式的求导即可。
B 麻将
首先需要建符合题意的自动机。
一开始我总是想把面子的状态记下来,然后这样建的自动机是 NFA,也不太能 dp;
后来重新考虑了一下自动机识别的东西是什么,对于识别了一段不下降的数字序列,同一个有无雀头和最后两个位置的待匹配数目,可以唯一确定其面子数目。
所以自动机上一个节点可以记录 (33) 个量作为一个等价类,(f[0/1][0sim 3][0sim 3]) 都记录对应情况面子数的最大值,(cnt) 记录有多少组数 (ge 2)。
对着 bfs 一波就可以确定自动机的形态?
在自动机上 dp ,可以求出 (g_i) 表示 (i) 轮没办法胡牌的方案数。
要算胡牌的期望轮次,可以考虑让所有方案在所有没办法胡牌的前缀产生 (1) 的贡献。
注:为什么不能从胡牌来考虑呢?因为一个前缀顺着做到第 (k) 位这里恰好能胡牌,不意味着 (k!) 种排列都满足恰好在 (k) 位这里胡牌。
#include<cstdio>
#include<map>
#include<cassert>
#include<cstring>
#include<algorithm>
#define rep(i, j, k) for(int i = j; i <= k; ++i)
#define rep2(i, j, k1, k2) for(int i = j; i <= k1 && i <= k2; ++i)
using namespace std;
typedef long long ll;
const int mod = 998244353;
int fexp(int a, int b) {
int x = 1;
for(; b; b >>= 1, a = 1ll * a * a % mod)
if(b & 1) x = 1ll * x * a % mod;
return x;
}
void upd(int &x, const int &y) {
x = x < y ? y : x;
}
void inc(int &x, const ll &y) {
x = (x + y) % mod;
}
int n;
struct node {
int a[2][3][3], cnt;
node() {
cnt = 0;
memset(a, -1, sizeof(a));
}
bool operator < (const node& p) const {
if(cnt != p.cnt)
return cnt < p.cnt;
rep(i, 0, 1) rep(j, 0, 2) rep(k, 0, 2)
if(a[i][j][k] != p.a[i][j][k])
return a[i][j][k] < p.a[i][j][k];
return 0;
}
node travel(int q) const {
node res;
rep(d, 0, 1)
rep(i, 0, 2)
rep(j, 0, 2)
if(~a[d][i][j])
rep2(k, 0, 2, q - i - j)
upd(res.a[d][j][k], a[d][i][j] + i + (q - i - j - k) / 3);
/*
内层 DP:
i 是上上个位置留下来组顺子用的,
j 是上个位置留下来组顺子用的,
则当前位置首先有 i + j 张牌要用来跟前面匹配,
剩下来 q - i - j 张,枚举留 k 张跟之后组顺子,
剩下来的尽量组顺子(多余的丢弃不用)
*/
res.cnt = cnt;
if(q >= 2) {
++res.cnt;
q -= 2;
rep(i, 0, 2)
rep(j, 0, 2)
if(~a[0][i][j])
rep(k, 0, q - i - j)
upd(res.a[1][j][k], a[0][i][j] + i + (q - i - j - k) / 3);
}
return res;
}
int check_end() {
if(cnt >= 7)
return 1;
rep(i, 0, 2) rep(j, 0, 2) {
if(a[1][i][j] >= 4)
return 1;
if(a[0][i][j] > 4)
a[0][i][j] = 4;
}
return 0;
}
}c[2333];
map<node, int> vis;
int tot, trans[2333][5];
void build()
{
c[1].a[0][0][0] = 0;
vis[c[1]] = tot = 1;
rep(i, 1, tot) {
assert(tot <= 2100);
node *p = c + i;
rep(j, 0, 4) {
node nxt = p->travel(j);
if(nxt.check_end())
trans[i][j] = 0; // can hu
else {
int e = vis[nxt];
if(e) trans[i][j] = e;
else trans[i][j] = vis[nxt] = ++tot, c[tot] = nxt;
}
}
}
}
int a[101], fac[401], C[5][5], dp[2][401][2333];
int main()
{
build();
scanf("%d", &n);
rep(i, 0, 4) rep(j, C[i][0] = 1, i)
C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
rep(i, 1, 13) {
int t;
scanf("%d %*d", &t);
++a[t];
}
fac[0] = 1;
rep(i, 1, 4 * n)
fac[i] = 1ll * fac[i - 1] * i % mod;
dp[0][0][1] = 1;
auto *f = dp[0], *g = dp[1];
rep(i, 0, n - 1) {
rep(j, 0, 4 * i) rep(k, 1, tot)
if(f[j][k]) {
rep(d, a[i + 1], 4)
inc(g[j + d][trans[k][d]], 1ll * f[j][k] * C[4 - a[i + 1]][d - a[i + 1]] % mod);
f[j][k] = 0;
}
swap(f, g);
}
int ans = 0;
rep(j, 13, 4 * n) {
int nothu = 0;
rep(k, 1, tot) inc(nothu, f[j][k]);
inc(ans, 1ll * nothu * fac[j - 13] % mod * fac[4 * n - j]);
}
rep(i, 1, 4 * n - 13)
ans = 1ll * ans * fexp(i, mod - 2) % mod;
ans = (ans + mod) % mod;
printf("%d
", ans);
return 0;
}
C 线段树
可以转化为每次操作以 (1/2) 的概率执行。
记录的 (f_u) 表示的是 (u) 这个线段树节点的期望 (tag) 数量 ((in [0,1]))。
记录的 (g_u) 表示的是 (u) 这个线段树节点往根走的路径上有无 (tag) 的0/1变量的期望 ((in [0,1]))。
对最下面的区间打标记传下去 (g'_u=frac{g_u+1}2)。
#include<cstdio>
const int mod = 998244353, N = 100005, nv2 = (mod + 1) / 2;
int n, m, all;
struct tag_T {
int a, b;
tag_T(int x = 1, int y = 0)
:a(x), b(y) {}
void operator += (const tag_T &q) {
int aa = 1ll * a * q.a % mod;
int bb = (1ll * q.a * b + q.b) % mod;
a = aa; b = bb;
}
};
struct seg {
seg *lc, *rc;
int a, b, sa;
tag_T c;
void pushup() {
sa = a;
if(lc) sa = (sa + lc->sa) % mod;
if(rc) sa = (sa + rc->sa) % mod;
}
void ac(const tag_T& d) {
c += d;
b = (1ll * d.a * b + d.b) % mod;
}
void pushdown() {
if(c.a != 1 || c.b)
lc->ac(c), rc->ac(c), c = tag_T();
}
}t[N * 2], *root; int tot;
seg* build(int l, int r)
{
auto *p = &t[tot++];
if(l < r)
p->lc = build(l, (l + r) / 2), p->rc = build((l + r) / 2 + 1, r);
return p;
}
void upd(seg *p, int l, int r, int pl, int pr)
{
if(l > pr || r < pl) {
p->a = 1ll * (p->a + p->b) * nv2 % mod;
} else if(l >= pl && r <= pr) {
p->a = 1ll * (p->a + 1) * nv2 % mod;
p->ac(tag_T(nv2, nv2));
} else {
p->a = 1ll * p->a * nv2 % mod;
p->b = 1ll * p->b * nv2 % mod;
p->pushdown();
int mid = (l + r) / 2;
upd(p->lc, l, mid, pl, pr);
upd(p->rc, mid + 1, r, pl, pr);
}
p->pushup();
}
int main () {
// freopen("sgt.in", "r", stdin);
// freopen("sgt.out", "w", stdout);
scanf("%d %d", &n, &m);
root = build(1, n);
all = 1;
while(m--)
{
int opt, l, r;
scanf("%d", &opt);
if(opt == 2) {
int q = 1ll * all * root->sa % mod;
printf("%d
", q);
}
else {
all = 2ll * all % mod;
scanf("%d %d", &l, &r);
upd(root, 1, n, l, r);
}
}
return 0;
}
D 语言
E 浙江省选
首先是求这些直线的半平面交,求出来若干个人在下凸壳上,就是第一名的人。
考虑其他人谁能当第二,在删掉第一名的人的新的下凸壳上,可以发现某个线段只要存在一个整点,上面覆盖了 (le 1) 个第一名的人,那就可以成为第二名。
把第二名的人删掉重复上述过程做。(O(nklog n))(细节极多)。