NFLSOJ 12365 - 「NOIP2021模拟赛0820南外」发怒(整除分块+点分治)

题面传送门

一道挺有意思的题,mod 一发出题人 ztr/se

首先我们有一个非常显然的 \(\Theta(nm)\) 的做法:设 \(dp_{u,j}\) 表示有多少个以 \(u\) 为根的树上连通块,满足连通块中所有 \(a_i\) 的乘积恰好为 \(j\),树上背包以下然后前缀和优化转移即可,一脸过不去的样子。

考虑优化。注意到我们在 DP 状态中专门记录一个目前连通块中所有元素的乘积,其实是很浪费的。因为我们只用关心 \(j\) 再乘个多少之后就能超过 \(m\),即 \(\lfloor\dfrac{m}{j}\rfloor\),因此我们考虑换一个 DP 状态的定义:\(dp_{u,j}\) 表示有多少个以 \(u\) 为根的树上连通块,满足 \(m\)​ 除以树上连通块中所有点的乘积等于 \(j\),这样根据整除分块那套理论,复杂度可以降到 \(\Theta(\sqrt{m})\),但是这样再跑树上背包就真的可以了吗?非也。因为我们知道 \(x=\lfloor\dfrac{m}{i}\rfloor\)\(y=\lfloor\dfrac{m}{j}\rfloor\),并不能直接推出 \(\lfloor\dfrac{m}{ij}\rfloor\)。因此无法合并两个背包,但是我们可以支持加入一个单点,这就导致我们直接树形背包变得相当棘手,因此我们只能另辟蹊径。

怎么办呢?这里有一个可能有些套路化的科技:考虑点分治(树上连通块计数也能点分治?想不到吧)。那么我们分经过当前分治中心的连通块和不经过当前分治中心的连通块处理。对于前者,由于是树上连通块,因此一个点如果被选入树上连通块中,那么它的父亲肯定要被选择,因此我们考虑一个类树形背包的 \(dp\)​​​,我们考虑设 \(dp_{i,j}\)​​​ 表示考虑了 DFS 序 \(\le i\)​​​ 的节点,它们的乘积 \(x\)​​​ 满足 \(\lfloor\dfrac{m}{x}\rfloor=j\)​,有多少个满足条件的连通块(这个 DFS 序可能略有点难理解,其实我们可以视作已经考虑过的点的 DP 状态​),考虑如何转移,我们 DFS 到一个节点 \(x\)​ 并遍历它的一个儿子 \(y\)​ 时,我们将在当前背包中加入 \(a_y\)​ 后的背包传给它的儿子 \(y\)​​,然后 \(y\)​ 带着这个 DP 值到 \(y\)​ 的子树内荡一圈,上推的时候,再将 \(y\)​ 的 DP 值传给 \(x\)​,这样我们的 DP 只有添加节点的操作,因此不会出现上面的问题。而对于后者,我们显然可以在后续的分治过程中考虑到,因此继续分治下去即可。

总结:

  • 对于状态中带下取整的 DP,可以考虑整除分块优化。
  • 对于那种合并不太行,但支持插入的树上连通块类 DP,可以考虑用点分治搞掉合并。
  • 对于父亲推给儿子式的 DP,可以考虑按 DFS 序处理。

时间复杂度 \(\Theta(n\sqrt{m}\log n)\)

const int MAXN = 2000;
const int MAXM = 1e6;
const int SQRT = 2000;
const int MOD = 1e9 + 7;
const int INF = 0x3f3f3f3f;
void add(int &x, int y) {((x += y) >= MOD) && (x -= MOD);}
int n, m, a[MAXN + 5], hd[MAXN + 5], to[MAXN * 2 + 5], nxt[MAXN * 2 + 5], ec = 0;
void adde(int u, int v) {to[++ec] = v; nxt[ec] = hd[u]; hd[u] = ec;}
int id[MAXM + 5], val[SQRT + 5], idcnt = 0;
int dp[MAXN + 5][SQRT + 5], res = 0;
int siz[MAXN + 5], mx[MAXN + 5], cent = 0;
bool vis[MAXN + 5];
void findcent(int x, int f, int totsiz) {
	mx[x] = 0; siz[x] = 1;
	for (int e = hd[x]; e; e = nxt[e]) {
		int y = to[e]; if (y == f || vis[y]) continue;
		findcent(y, x, totsiz); siz[x] += siz[y]; chkmax(mx[x], siz[y]);
	} chkmax(mx[x], totsiz - siz[x]);
	if (mx[x] < mx[cent]) cent = x;
}
void getdp(int x, int f) {
	for (int e = hd[x]; e; e = nxt[e]) {
		int y = to[e]; if (y == f || vis[y]) continue;
		for (int i = 1; i <= idcnt; i++) add(dp[y][id[val[i] / a[y]]], dp[x][i]);
		getdp(y, x);
		for (int i = 1; i <= idcnt; i++) add(dp[x][i], dp[y][i]), dp[y][i] = 0;
	}
}
void divcent(int x) {
	vis[x] = 1; dp[x][id[m / a[x]]] = 1; getdp(x, 0);
	for (int i = 1; i <= idcnt; i++) add(res, dp[x][i]), dp[x][i] = 0;
	for (int e = hd[x]; e; e = nxt[e]) {
		int y = to[e]; if (vis[y]) continue;
		cent = 0; findcent(y, x, siz[y]);
		divcent(cent);
	}
}
int main() {
	freopen("fn.in", "r", stdin);
	freopen("fn.out", "w", stdout);
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
	for (int i = 1, u, v; i < n; i++) scanf("%d%d", &u, &v), adde(u, v), adde(v, u);
	for (int l = 1, r; l <= m; l = r + 1) {
		r = (m / (m / l)); id[m / l] = ++idcnt;
		val[idcnt] = m / l;
	}
	mx[0] = INF; findcent(1, 0, n); divcent(cent);
	printf("%d\n", res);
	return 0;
}
posted @ 2021-11-15 13:27  tzc_wk  阅读(58)  评论(0)    收藏  举报