• 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搜索

  • 相关阅读:
    IDEA安装和JDK的配置
    IDEA里面创建maven项目,依赖
    JSON的简单书写(初学),使用Java语言反序列化
    Spring中的转换器:Converter
    关于Spring MVC中的表单标签库的使用
    解决Spring框架下中文乱码的问题
    SpringMVC中使用DispatcherServlet
    八大排序算法:冒泡排序
    总线与南桥和北桥
    设备驱动与控制器 I/O
  • 原文地址:https://www.cnblogs.com/bestwyj/p/13023425.html
Copyright © 2020-2023  润新知