ZJOI2019 简记

A 开关

考场上一直在那里想消元技巧,然后垫底了。

记一个开关 (i)(j) 步被打开的指数型生成函数:

[F_i(x)=sum_{jge 0}^infty {1+(-1)^{j+s_i}over 2}p_i^j {x^jover j!} ]

[={e^{p_ix}+(-1)^{s_i}e^{-p_ix}over 2} ]

显然是带标号球的拼接:

[F(x)=prod_{i=1}^n F_i(x) ]

(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)多项式乘法逆的形式,即

[h(x)={f(x)over g(x)} ]

有不少题目都可以这么容斥:
比如坐标系中游走 (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) 的贡献。

[ans=1+sum_{i=1}^N {g_ii!(N-i)!over N!} ]

注:为什么不能从胡牌来考虑呢?因为一个前缀顺着做到第 (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))(细节极多)。

F Minimax搜索

原文地址:https://www.cnblogs.com/bestwyj/p/13023425.html