PKUSC2022 题解

耻辱终须自己解决。

因为官网题目已经没有提交入口了,所以所有题目均根据 InfOJ 的 P184~189 民间数据验证。

感谢 feecle6418 哥哥的帮助/bx

Rating

  • 给定两个初始为 \(0\) 的变量 \(x,y\),进行如下随机过程:
    • 每一步根据给定的概率分布从 \([-m,m]\) 中得到整数 \(d\)
    • 对于 \(x,y\) 中的较小值 \(z\)\(z\gets \max(0,z+d)\)
  • \(\max(x,y)\geq n\) 时终止过程,求终止时的期望步数。
  • \(n\leq 1000,m\leq 50\)

对我来说还是很 hard 的期望题,可能是套路积累不够吧。

虽然有两个 rating,但是实际上每次都是两个 rating 交替进行连续的移动,直至较小的超过较大的。

这启发我们考虑单一变量的变化过程。

\(f(i,j)\) 表示 rating \(i\) 第一次达到 \(>i\) 时为 \(i+j\) 的概率,注意 \(j\in[1,m]\)。按照 \(i\) 从小到大转移。

转移时根据第一步的走向 \(k\in[-m,m]\),如果 \(k<0\) 就利用先前的 \(f(i+k,*)\) 转移,可以 \(O(m^2)\) 线性 DP,注意 \(k=0\) 要列方程。

之后设 \(g(i,j)\) 表示 \(\max(x,y)=i,\min(x,y)=i-j\) 的答案,利用预处理的 \(f\) 可以轻松转移。

注意到题目求的是期望,所以利用期望的线性性将 \(f,g\) 均改成 \(\text{pair}(P,E)\),重定义加法和乘法即可。复杂度 \(O(nm^2)\)

官方题解针对 \(f\) 的转移需要定义 \(h(i,j,k)\) 表示:\(i-j\to i+k\) 且不经过 \(>i\) 的中间值的概率,来辅助转移,但似乎不方便扩展到期望?

(懂的哥哥教教我啊 QwQ)

code
#include<bits/stdc++.h>
typedef long long ll;
#define rep(i, a, b) for(int i = (a); i <= (b); i ++)
#define per(i, a, b) for(int i = (a); i >= (b); i --)
#define Ede(i, u) for(int i = head[u]; i; i = e[i].nxt)
using namespace std;

typedef pair<int, int> pii;
#define mp make_pair
#define fi first
#define se second

const int P = 998244353;
inline int plu(int x, int y) {return x + y >= P ? x + y - P : x + y;}
inline int del(int x, int y) {return x - y <  0 ? x - y + P : x - y;}
inline void add(int &x, int y) {x = plu(x, y);}
inline void sub(int &x, int y) {x = del(x, y);}

inline int read() {
	int x = 0, f = 1; char c = getchar();
	while(c < '0' || c > '9') f = (c == '-') ? - 1 : 1, c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
	return x * f;
}

inline int qpow(int a, int b) {
	int s = 1;
	for(; b; b >>= 1, a = 1ll * a * a % P) if(b & 1) s = 1ll * s * a % P;
	return s;
}
inline pii operator + (const pii &a, const pii &b) {return mp(plu(a.fi, b.fi), plu(a.se, b.se));}
inline pii operator * (const pii &a, const pii &b) {
	return mp(1ll * a.fi * b.fi % P, plu(1ll * a.se * b.fi % P, 1ll * b.se * a.fi % P));
}

const int N = 1010, M = 55;
int n, m, p[M << 1];
pii w[M << 1], f[N][M], g[N + M][M];

int main() {
	n = read(), m = read();
	int inv = qpow(1e8, P - 2);
	rep(i, 0, m << 1) p[i] = 1ll * inv * read() % P;
	
	rep(i, 0, n - 1) {
		rep(j, 0, m << 1) w[j] = mp(0, 0);
		rep(j, 0, m << 1) add(w[max(-i, j - m) + m].fi, p[j]);
		rep(j, 0, m << 1) w[j].se = w[j].fi;

		rep(j, max(0, m - i), m - 1) rep(k, 1, m) w[j + k] = w[j + k] + w[j] * f[i + j - m][k];
		int inv = qpow(del(1, w[m].fi), P - 2);
		rep(j, 1, m) {
			f[i][j].fi = 1ll * w[j + m].fi * inv % P;
			f[i][j].se = 1ll * plu(w[j + m].se, 1ll * w[m].se * f[i][j].fi % P) * inv % P;
		}
	}

	g[0][0] = mp(1, 0);
	rep(i, 0, n - 1) per(j, min(i, m), 0) {
		rep(k, 1, j) g[i][j - k] = g[i][j - k] + g[i][j] * f[i - j][k];
		rep(k, j + 1, m)
			g[i - j + k][k - j] = g[i - j + k][k - j] + g[i][j] * f[i - j][k];
	}
	
	int ans = 0;
	rep(i, n, n + m) rep(j, 0, m) add(ans, g[i][j].se);
	printf("%d\n", ans);
	
	return 0;
}

Easygeo

  • \(n\) 个线段,第 \(i\) 条线段连接 \((a_i,b_i),(c_i,d_i)\),保证:

    • 对于任意线段满足 \(c_i>a_i,d_i<b_i\)
    • 对于任意两条线段均不相交,包括端点处。
  • \(q\) 次询问,每次给出一个点 \((x_i,y_i)\),询问:

    对于所有 \(n\) 条线段,其落在 \((-C,-C)\) 为左下角,\((x_i,y_i)\) 为右上角的矩形内部分的长度之和。

  • \(C=3\times 10^5,|a_i|,|b_i|,|c_i|,|d_i|,|x_i|,|y_i|\leq 3\times 10^5,n\leq 10^5,q\leq 1.5\times 10^5\)

仔细思考下来发现是比较基础的数据结构题?

根据一个 subtask 引出的正解,那个 subtask 满足 \(\forall i,a_i+b_i=c_i+d_i\)。也就是说所有线段斜率相等且与 \(y=-x\) 平行。

不难想到根据截距从小到大排序分层,将每个询问插入到它所在的两层之间,显然询问只需要考虑左下及之前层的线段,扫描线扫一遍。

将询问转化为总长度 - 矩形外的长度,发现这样扫描之后不存在同时有 \(x,y\) 均比询问点大的线段在询问之前被加入。

那么对横坐标和纵坐标分别维护线段树,支持区间加和区间查询即可。

对于正解,延续思路,无非是确定一个顺序,使得:

  • 在一个询问之前加入的线段一定不存在 \(x,y\) 同时比询问点大的情况。
  • 在一个询问之后加入的线段对这个询问一定没贡献。

发现这构成一个偏序关系,因为线段之间无交所以这样的顺序总是存在的。

具体的,在 \(x\) 维进行扫描线,在 \(a_i\) 处加入线段,\(c_i\) 处删除。因为线段不交,同一时间存在的线段的 \(y\) 的相对大小关系一定固定。

之后每次新加入线段/询问时,将它插入到后继的前一个即可。

因为未被加入的线段之后会考虑,已经被删除的线段不可能有点在新加入的线段/询问的右上方,故这个做法正确。

实现上用平衡树维护线段集合,每次平衡树上二分找后继,双向链表维护构造序列即可。

之后就和 subtask 做法一模一样了。复杂度 \(O((n+q)\log C)\)。写了很久然后一遍过感觉怪怪的?

code
#include<bits/stdc++.h>
typedef long long ll;
#define rep(i, a, b) for(int i = (a); i <= (b); i ++)
#define per(i, a, b) for(int i = (a); i >= (b); i --)
#define Ede(i, u) for(int i = head[u]; i; i = e[i].nxt)
using namespace std;

#define eb emplace_back
typedef pair<int, int> pii;
#define mp make_pair
#define fi first
#define se second

inline int read() {
	int x = 0, f = 1; char c = getchar();
	while(c < '0' || c > '9') f = (c == '-') ? - 1 : 1, c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
	return x * f;
}

const double eps = 1e-12;
const int N = 6e5 + 10, Delta = 3e5 + 1;
int n, m, v = 6e5 + 1, arc[N];
struct node {int a, b, c, d, idx;} p[N];

namespace chain {
	int pre[N], nxt[N], head, tail;
	void push(int v, int k) {
		if(! head) {head = tail = v; return;}
		if(! k) {pre[v] = tail, nxt[tail] = v, tail = v; return;}
		pre[v] = pre[k];
		nxt[v] = k;
		nxt[pre[v]] = v;
		pre[k] = v;
		if(k == head) head = v;
	}
	void solve() {
		int o = head, tot = 0;
		while(o) arc[++ tot] = o, o = nxt[o];
	}
}

vector<int> g[N];
mt19937 myrand(19260817);
int tot, root, pos[N];
struct tree {int l, r, v, siz, par, rnd;} t[N];

int init(int v) {
	pos[v] = ++ tot;
	t[tot] = (tree) {0, 0, v, 1, 0, (int) (myrand() >> 1)};
	return tot;
}

void pushup(int p) {
	t[p].siz = t[t[p].l].siz + t[t[p].r].siz + 1;
	if(t[p].l) t[t[p].l].par = p;
	if(t[p].r) t[t[p].r].par = p;
}

void spl(int o, int k, int &x, int &y) {
	if(! o) return x = y = 0, void();
	if(t[t[o].l].siz + 1 <= k)
		x = o, spl(t[o].r, k - t[t[o].l].siz - 1, t[x].r, y), pushup(x);
	else y = o, spl(t[o].l, k, x, t[y].l), pushup(y);
}

int mer(int x, int y) {
	if(! x || ! y) return x | y;
	if(t[x].rnd < t[y].rnd)
		return t[x].r = mer(t[x].r, y), pushup(x), x;
	else return t[y].l = mer(x, t[y].l), pushup(y), y;
}

bool check(int v, int x, int y) {
	double k = (p[v].b - p[v].d) / (double) (p[v].c - p[v].a);
	double z = p[v].b - k * (x - p[v].a);
	return (fabs(z - y) < eps || z + eps > (double) y);
}

int find(int p, int x, int y) {
	if(! p) return 0;
	if(check(t[p].v, x, y)) {
		int dat = find(t[p].l, x, y); return dat ? dat : t[p].v;
	} else return find(t[p].r, x, y);
}

int calc(int v) {
	int o = pos[v], k = t[t[o].l].siz + 1;
	while(o != root) {
		if(o == t[t[o].par].r) k += t[t[o].par].siz - t[o].siz;
		o = t[o].par;
	}
	return k;
}

void insert(int v) {
	int k = find(root, p[v].a, p[v].b);
	chain::push(v, k);
	if(! p[v].idx) {
		if(! k) root = mer(root, init(v));
		else {
			int c = calc(k), x;
			spl(root, c - 1, root, x);
			root = mer(root, mer(init(v), x));
		}
	}
}

void remove(int v) {
	int k = calc(v), x, y;
	spl(root, k - 1, root, x), spl(x, 1, x, y);
	root = mer(root, y);
}

void getarc() {
	rep(i, 1, n + m) {
		g[p[i].a].eb(i);
		if(! p[i].idx) g[p[i].c].eb(-i);
	}
	rep(i, 1, v) {
		sort(g[i].begin(), g[i].end(), [](int x, int y) {
			if(x < 0 || y < 0) return x < y; else return p[x].b > p[y].b;
		});
		for(int o : g[i]) if(o < 0) remove(-o);
		for(int o : g[i]) if(o > 0) insert( o);
	}
	chain::solve();
}

double ans[N];

struct segmenttree {
	double dat[N << 2], add[N << 2];
	
	void push(int p, int l, int r, double v) {dat[p] += (r - l + 1) * v, add[p] += v;}
	void pushdw(int p, int l, int r) {
		int mid = (l + r) >> 1;
		if(add[p] > eps)
			push(p << 1, l, mid, add[p]), 
			push(p << 1 | 1, mid + 1, r, add[p]), add[p] = 0.0;
	}
	void pushup(int p) {dat[p] = dat[p << 1] + dat[p << 1 | 1];}
	
	void moi(int p, int l, int r, int L, int R, double v) {
		if(L <= l && r <= R) return push(p, l, r, v);
		pushdw(p, l, r); int mid = (l + r) >> 1;
		if(L <= mid) moi(p << 1, l, mid, L, R, v);
		if(R >  mid) moi(p << 1 | 1, mid + 1, r, L, R, v);
		pushup(p);
	}
	
	double que(int p, int l, int r, int L, int R) {
		if(L <= l && r <= R) return dat[p];
		pushdw(p, l, r); int mid = (l + r) >> 1; double cur = 0.0;
		if(L <= mid) cur += que(p << 1, l, mid, L, R);
		if(R >  mid) cur += que(p << 1 | 1, mid + 1, r, L, R);
		return cur;
	}
	
} seg[2];

int main() {
	n = read();
	rep(i, 1, n)
		p[i].a = read() + Delta, p[i].b = read() + Delta, 
		p[i].c = read() + Delta, p[i].d = read() + Delta;
	m = read();
	rep(i, 1, m)
		p[n + i].a = read() + Delta, 
		p[n + i].b = read() + Delta, p[n + i].idx = i;
	getarc();
	
	double all = 0.0;
	rep(i, 1, n + m) {
		int o = arc[i];
		if(p[o].idx)
			ans[p[o].idx] = all - seg[0].que(1, 1, v, p[o].a, v) - seg[1].que(1, 1, v, p[o].b, v);
		else {
			int x = p[o].c - p[o].a;
			int y = p[o].b - p[o].d;
			double len = sqrt((double) x * x + (double) y * y);
			seg[0].moi(1, 1, v, p[o].a, p[o].c - 1, len / (double) x);
			seg[1].moi(1, 1, v, p[o].d, p[o].b - 1, len / (double) y);
			all += len;
		}
	}
	
	rep(i, 1, m) printf("%.11f\n", ans[i]);
	return 0;
}

Cat

  • 给定 \(n\) 只猫,愿意被 rua 的猫构成一个集合,给出所有可能集合的概率。
  • 对于每个集合,可以把 rua 猫的概率按任意策略分配给它里面的猫。
  • 最大化 \(\min_i \dfrac{\text{rua}(i)}{\text{willing}(i)}\),其中 \(\text{rua}(i)\) 表示 \(i\) 被 rua 的概率,\(\text{willing}(i)\) 表示 \(i\) 出现在集合中的概率。
  • \(n\leq 20\)

我当时在干什么.jpg

首先想到二分答案 \(c\),能够得到一个线性规划问题判定是否有解,并且发现能轻松转化成一个二分图匹配满流的问题。

一边是 \(2^n\) 个集合,一边是 \(n\) 个点,拿 \(n\) 个点的部分暴力做 Hall 的判定即可,根据 Hall 的扩展结论甚至可以去掉二分答案的过程。

复杂度 \(O(n2^n)\)

code
#include<bits/stdc++.h>
typedef long long ll;
#define rep(i, a, b) for(int i = (a); i <= (b); i ++)
#define per(i, a, b) for(int i = (a); i >= (b); i --)
#define Ede(i, u) for(int i = head[u]; i; i = e[i].nxt)
using namespace std;

inline int read() {
	int x = 0, f = 1; char c = getchar();
	while(c < '0' || c > '9') f = (c == '-') ? - 1 : 1, c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
	return x * f;
}

const int N = 20;
int n; double p[N], f[1 << N], g[1 << N];

int main() {
	n = read();
	rep(i, 0, (1 << n) - 1) scanf("%lf", &f[i]);
	rep(i, 0, (1 << n) - 1) rep(o, 0, n - 1) if(i >> o & 1) p[o] += f[i];
	rep(i, 1, (1 << n) - 1) g[i] = g[i ^ (i & -i)] + p[__builtin_ctz(i & -i)];
	for(int o = 2, k = 1; o <= (1 << n); o <<= 1, k <<= 1)
		for(int i = 0; i < (1 << n); i += o) rep(j, 0, k - 1) f[i + j + k] += f[i + j];
	double ans = 1.0;
	rep(s, 0, (1 << n) - 1) ans = min(ans, (f[(1 << n) - 1] - f[((1 << n) - 1) ^ s]) / g[s]);
	printf("%.10f\n", ans);
	return 0;
}

Function

  • \(V\) 表示集合 \(\{1,2,\cdots,m\}\)
  • 从所有 \(m^m\)\(V\to V\) 的函数中随机选取两个 \(f,g\)
  • \(V\) 中等概率随机 \(2n\) 个数字 \(x_1,\cdots,x_n,y_1,\cdots,y_n\)
  • 计算 \(\and f(x_i)=g(y_i)\) 发生的概率。
  • \(n\leq 40,m\leq 10^9\)

有趣的计数题!

如果能枚举所有 \(m^{2n}\) 种数字选择,计算合法的概率是容易的,表达到一张二分图上,假设连通块个数为 \(c\),概率就是 \(m^{c-2m}\)

考虑对于二分图计数,虽然 \(c\) 可能很大,但是发现大量连通块是孤点。

如果只考虑非孤点的连通块个数为 \(c'\)\(x=|\{x_i\}|,y=|\{y_i\}|\),概率同样能够计算,为 \(m^{2m-x-y+c}/m^{2m}\)

\(g(i,j,e)\) 表示 \(x=i,y=j\) 且有 \(e\) 条边的二分图的 \(m^c\) 之和,则:

\[\text{Ans}=m^{-2n}m^{-2m}\sum_{e=1}^n s(n,e)e!\sum_{i=1}^n\sum_{j=1}^n g(i,j,e)\binom{m}{i}\binom{m}{j}m^{2m-i-j} \]

\(g\) 的递推只需要固定一个关键点,利用 \(f(i,j,e)\) 表示 \(x=i,y=j\) 且有 \(e\) 条边的连通二分图个数计算即可。

\(f\) 的递推同样是固定关键点,利用自身简单容斥即可。

复杂度 \(O(n^6)\) 但是跑不满,cerr 了一下极限数据 \(g\) 的计算量大概在 \(5\times 10^7\)

据说还有一种拆分数相关的做法,懂的哥哥教教我啊 QwQ

code
#include<bits/stdc++.h>
typedef long long ll;
#define rep(i, a, b) for(int i = (a); i <= (b); i ++)
#define per(i, a, b) for(int i = (a); i >= (b); i --)
#define Ede(i, u) for(int i = head[u]; i; i = e[i].nxt)
using namespace std;

int P;
inline int plu(int x, int y) {return x + y >= P ? x + y - P : x + y;}
inline int del(int x, int y) {return x - y <  0 ? x - y + P : x - y;}
inline void add(int &x, int y) {x = plu(x, y);}
inline void sub(int &x, int y) {x = del(x, y);}
inline int qpow(int a, int b) {int s = 1; for(; b; b >>= 1, a = 1ll * a * a % P) if(b & 1) s = 1ll * s * a % P; return s;}

inline int read() {
	int x = 0, f = 1; char c = getchar();
	while(c < '0' || c > '9') f = (c == '-') ? - 1 : 1, c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
	return x * f;
}

const int N = 45;
int n, m, s[N][N], fac[N], binom[N], c[N * N][N * N], f[N][N][N], g[N][N][N];

void prework() {
	fac[0] = 1;
	rep(i, 1, n) fac[i] = 1ll * fac[i - 1] * i % P;
	binom[0] = 1;
	rep(i, 1, min(n, m)) binom[i] = 1ll * binom[i - 1] * (m - i + 1) % P * qpow(i, P - 2) % P;
	c[0][0] = 1;
	rep(i, 1, n * n) {
		c[i][0] = c[i][i] = 1;
		rep(j, 1, i - 1) c[i][j] = plu(c[i - 1][j], c[i - 1][j - 1]);
	}
	s[0][0] = 1;
	rep(i, 1, n) rep(j, 1, i) s[i][j] = plu(s[i - 1][j - 1], 1ll * j * s[i - 1][j] % P);
}

int main() {
	n = read(), m = read(), P = read();
	
	prework();
	
	f[1][0][0] = 1;
	rep(i, 1, n) rep(j, 1, n) rep(k, i + j - 1, min(i * j, n)) {
		f[i][j][k] = c[i * j][k];
		rep(x, 1, i) rep(y, 0, j) if(x + y != i + j) rep(z, x + y - 1, min(x * y, k))
			sub(f[i][j][k], 1ll * f[x][y][z] * c[i - 1][x - 1] % P * c[j][y] % P * c[(i - x) * (j - y)][k - z] % P);
	}
	
	g[0][0][0] = 1;
	rep(i, 1, n) rep(j, 1, n) rep(k, max(i, j), min(i * j, n))
	rep(x, 1, i) rep(y, 1, j) rep(z, x + y - 1, min(x * y, k))
		add(g[i][j][k], 1ll * g[i - x][j - y][k - z] * f[x][y][z] % P * m % P * c[i - 1][x - 1] % P * c[j][y] % P);
	
	int ans = 0;
	rep(k, 1, n) {
		int cur = 0;
		rep(i, 1, min(n, m)) rep(j, 1, min(n, m)) add(cur, 1ll * g[i][j][k] * binom[i] % P * binom[j] % P * qpow(m, 2 * m - i - j) % P);
		add(ans, 1ll * cur * s[n][k] % P * fac[k] % P);
	}	
	
	int mul = qpow(qpow(m, n + m), P - 2);
	ans = 1ll * ans * mul % P * mul % P;
	printf("%d\n", ans);
	return 0;
}

Colorfultree

  • 给定一棵树,边有颜色。
  • 定义一个路径合法为:路径上的颜色只在这个路径上出现。
  • 对于每个点,求出删去这个点后仍然合法的路径的最大长度。
  • 定义仍然合法为:在原树合法,且不经过被删去的点。
  • \(n\leq 10^5\)

这道题由众多经典 trick 组成,受益匪浅。

核心思想:对每种颜色考虑随机化边权,使得同色异或和为 \(0\)。注意随机化值域要达到 \(>n^2\),例如 mt19937_64

这样大概率一条链合法等价于边权异或和为 \(0\)

令每个点的颜色为它到根的异或和,那么如果没有删点相当于求全局最远同色点对。

利用直径的性质,容易维护一个数据结构,支持:\(O(\log n)\) 加点、\(O(1)\) 查询点集内的最远同色点对。

对于不经过某个点的答案,经典套路是考虑全局的最长路径。

不在路径上的点答案确定,路径上的点的答案分:自己子树、左链、右链 三部分,均可利用上述数据结构 \(O(n)\) 次加点。

总时间复杂度 \(O(n\log n)\)

code
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
#define rep(i, a, b) for(int i = (a); i <= (b); i ++)
#define per(i, a, b) for(int i = (a); i >= (b); i --)
#define Ede(i, u) for(int i = head[u]; i; i = e[i].nxt)
using namespace std;

#define eb emplace_back
#define mp make_pair
#define fi first
#define se second

inline int read() {
	int x = 0, f = 1; char c = getchar();
	while(c < '0' || c > '9') f = (c == '-') ? - 1 : 1, c = getchar();
	while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
	return x * f;
}

const int N = 1e5 + 10;
int n, m, q;
struct edge {int u, v, c;} e[N];
vector<int> h[N];
vector<pair<int, ull>> g[N];
mt19937_64 myrand(19260817);

void add(edge e, ull w) {int u = e.u, v = e.v; g[u].eb(mp(v, w)), g[v].eb(mp(u, w));}

void init() {
	n = read(), q = read();
	map<int, int> num; int tot = 0;
	rep(i, 1, n - 1) {
		e[i].u = read(), e[i].v = read(), e[i].c = read();
		if(num.find(e[i].c) == num.end()) num[e[i].c] = ++ tot;
		int cur = num[e[i].c];
		h[cur].eb(i);
	}
	rep(i, 1, tot) {
		int len = (int) h[i].size();
		ull all = 0;
		rep(j, 0, len - 2) {ull w = myrand(); add(e[h[i][j]], w), all ^= w;}
		add(e[h[i][len - 1]], all);
	}
}

int dep[N], par[N], col[N], siz[N], son[N]; ull val[N], uni[N];
void dfs1(int u, int fa) {
	par[u] = fa, siz[u] = 1, dep[u] = dep[fa] + 1;
	for(auto e : g[u]) if(e.fi != fa) {
		int v = e.fi; ull w = e.se;
		val[v] = val[u] ^ w, dfs1(v, u), siz[u] += siz[v];
		if(! son[u] || siz[v] > siz[son[u]]) son[u] = v;
	}
}

int top[N];
void dfs2(int u, int tp) {
	top[u] = tp;
	if(son[u]) dfs2(son[u], tp);
	for(auto e : g[u])
		if(e.fi != par[u] && e.fi != son[u]) dfs2(e.fi, e.fi);
}

int getdis(int x, int y) {
	int cur = dep[x] + dep[y];
	while(top[x] != top[y]) {
		if(dep[top[x]] < dep[top[y]]) swap(x, y);
		x = par[top[x]];
	}
	int z = dep[x] < dep[y] ? x : y;
	return cur - 2 * dep[z];
}

namespace bac {
	int s[N], t[N], d[N];
	int smx, tmx, dmx; bool vis[N];
	vector<int> used;
	void init() {
		smx = tmx = dmx = 0;
		for(int i : used) s[i] = t[i] = d[i] = 0, vis[i] = false;
		used.clear();
	}
	void calc(int x) {
		int c = col[x];
		if(! vis[c]) vis[c] = true, used.eb(c);
		if(! s[c]) return s[c] = x, void();
		if(! t[c]) {
			t[c] = x; d[c] = getdis(x, s[c]);
			if(d[c] > dmx) smx = s[c], tmx = t[c], dmx = d[c];
			return;
		}
		int cur = 0, mxp = d[c], flag = 0;
		if((cur = getdis(x, s[c])) > mxp) flag = 1, mxp = cur;
		if((cur = getdis(x, t[c])) > mxp) flag = 2, mxp = cur;
		if(flag == 1) t[c] = x;
		if(flag == 2) s[c] = x;
		d[c] = mxp;
		if(d[c] > dmx) smx = s[c], tmx = t[c], dmx = d[c];
	}
}

bool vis[N];
void dfs3(int u) {vis[u] = true, bac::calc(u); for(auto e : g[u]) if(! vis[e.fi]) dfs3(e.fi);}

int ans[N];

int main() {
	init();
	dfs1(1, 0);
	dfs2(1, 1);
	rep(i, 1, n) uni[i] = val[i];
	sort(uni + 1, uni + n + 1);
	m = unique(uni + 1, uni + n + 1) - (uni + 1);
	rep(i, 1, n) col[i] = lower_bound(uni + 1, uni + m + 1, val[i]) - uni;
	
	bac::init(); dfs3(1);
	rep(i, 1, n) vis[i] = false;
	int s = bac::smx, t = bac::tmx, d = bac::dmx;
	
	if(! d) {while(q --) read(), puts("0"); return 0;}
	
	vector<int> lef, rig, arc;
	while(s != t) {
		if(dep[s] >= dep[t]) lef.eb(s), s = par[s];
		else rig.eb(t), t = par[t];
	}
	lef.eb(s), reverse(rig.begin(), rig.end());
	for(int o : lef) arc.eb(o);
	for(int o : rig) arc.eb(o);
	for(int o : arc) vis[o] = true;
	rep(i, 1, n) if(! vis[i]) ans[i] = d;

	for(int o : arc) for(auto e : g[o])
		if(! vis[e.fi]) bac::init(), dfs3(e.fi), ans[o] = max(ans[o], bac::dmx);
	
	rep(i, 1, n) vis[i] = false;
	for(int o : arc) vis[o] = true;
	
	int len = (int) arc.size();
	bac::init();
	rep(i, 1, len - 1) {
		int o = arc[i], c = arc[i - 1];
		bac::calc(c);
		for(auto e : g[c]) if(! vis[e.fi]) dfs3(e.fi);
		ans[o] = max(ans[o], bac::dmx);
	}

	rep(i, 1, n) vis[i] = false;
	for(int o : arc) vis[o] = true;

	bac::init();
	per(i, len - 2, 0) {
		int o = arc[i], c = arc[i + 1];
		bac::calc(c);
		for(auto e : g[c]) if(! vis[e.fi]) dfs3(e.fi);
		ans[o] = max(ans[o], bac::dmx);
	}
	
	while(q --) {int u = read(); printf("%d\n", ans[u]);}
	return 0;
}

Mahjong

  • 给出 \(3\) 种花色的牌共 \(13\) 张,每个花色有数字 \(1\sim 9\),也即总共有 \(27\) 种牌。

    初始局面每种至多一张,全局每种牌 \(4\) 张。

  • 定义两种牌:

    • 对子:两张同色同数的牌。
    • 面子:三张同色同数的牌,或三张同色且数字连续的牌。
  • 定义两种胡法:

    • 七对子:即有 \(7\) 个对子,且没有重复对子。
    • 面子手:有 \(4\) 个面子和一个雀头(对子)。
  • 定义听牌:从牌堆中随机摸一张牌,在检查是否当前的 \(14\) 张牌胡牌后,任意弃置一张牌。

  • 求当前局面想要胡牌至少需要听牌多少次。

  • 保证 \(\text{ans}\leq 5\)

感觉但凡 NOI 前补了这题 Day1T2 也不至于一点思路没有,现在补题顺序反了感觉这题异常简单(

仍然考虑给定局面,判定是否胡牌。

七对子就看是否所有牌的数量 \(=0/2\)

面子手则需要 DP,设 \(h(i,j,k,a,b)\) 表示:

考虑了前 \(i\) 种牌,已有 \(j\) 个面子,\(k=0/1\) 表示是否有雀头,\(a,b\) 表示分别向 \(i+1,i+2\) 借了多少张。

这个局面能否达到。

不能跨花色借,所以花色边界处要强制 \(a>0\or b>0\) 的为 false,最后看 \(h(27,4,1,0,0)\) 是否为 true 即可。

对于最少听牌的计数只需要将 bool 改为最少听牌次数。

设某种牌初始有 \(a\) 张,DP 决策希望有 \(b\) 张,那么直接计入代价 \(|a-b|\),最后 \(/2\) 即可。

或者甚至可以仿照今年 Day1T2 直接写一个 DP of DP 的自动机式转移,但是这样外层变成了一个线性 DP 感觉挺傻瓜的(

code
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
#define rep(i, a, b) for(int i = (a); i <= (b); i ++)
#define per(i, a, b) for(int i = (a); i >= (b); i --)
#define Ede(i, u) for(int i = head[u]; i; i = e[i].nxt)
using namespace std;

const int inf = 0x3f3f3f3f;
int dat[30], f[30][10], g[30][5][2][5][5];
char s[30];

void cmin(int &x, int y) {x = x < y ? x : y;}

int main() {
	scanf("%s", s + 1); int i = 0;
	for(i ++; s[i] != 'm'; i ++) dat[s[i] - '0'] ++;
	for(i ++; s[i] != 'p'; i ++) dat[s[i] - '0' + 9] ++;
	for(i ++; s[i] != 's'; i ++) dat[s[i] - '0' + 18] ++;
	
	memset(f, 0x3f, sizeof(f)), f[0][0] = 0;
	rep(i, 0, 26) rep(j, 0, 7) if(f[i][j] != inf) {
		cmin(f[i + 1][j], f[i][j] + dat[i + 1]);
		if(j < 7) cmin(f[i + 1][j + 1], f[i][j] + abs(dat[i + 1] - 2));
	}
	
	memset(g, 0x3f, sizeof(g)), g[0][0][0][0][0] = 0;
	rep(i, 0, 26) rep(j, 0, 4) rep(k, 0, 1) rep(a, 0, 4) rep(b, 0, 4)
		if(g[i][j][k][a][b] != inf) {
			if((i == 9 || i == 18) && (a || b)) continue;
			if((i == 8 || i == 17) && b) continue;
			rep(c, 0, 4 - a) {
				int v = abs(dat[i + 1] - (a + c));
				if(j + c <= 4 && b + c <= 4)
					cmin(g[i + 1][j + c][k][b + c][c], g[i][j][k][a][b] + v);
				if(c >= 2 && ! k && j + c - 2 <= 4 && b + c - 2 <= 4)
					cmin(g[i + 1][j + c - 2][1][b + c - 2][c - 2], g[i][j][k][a][b] + v);
				if(c >= 3 && j + c - 2 <= 4 && b + c - 3 <= 4)	
					cmin(g[i + 1][j + c - 2][k][b + c - 3][c - 3], g[i][j][k][a][b] + v);
			}
		}
	
	int ans = min(f[27][7] >> 1, g[27][4][1][0][0] >> 1);
	printf("%d\n", ans);
	return 0;
}
posted @ 2022-09-08 16:40  LPF'sBlog  阅读(566)  评论(0编辑  收藏  举报