Logo Un1quAIoid 的博客

博客

标签
暂无

P9340 JOISC2023 D3T3 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2023-05-23 16:38:01

传送门:P9340


题目大意:

给定一棵 $n$ 个点的树以及一个序列 $a_{1,\dots,m}$,$q$ 次询问包含 $a_{l,\dots,r}$ 中所有点的最小连通块大小。


回滚莫队一点也不优美,所以这里是 $O(n \log^2 n)$ 解法。

step1

不妨将树的根定为 $1$ 号结点。

不难看出题目就是求区间所有点构成的虚树大小,将在区间 $[l,r]$ 中出现过的点染黑,其它点染白,可以将虚树大小转化成两部分:

  1. 加上所有点 $u$ 的个数,满足 $u$ 的子树中存在至少一个黑点
  2. 减去所有点 $u$ 的个数,满足 $u$ 的子树之外(包括 $u$)全部为白点

第二部分实际上就是虚树的根的深度,这是好算的,虚树的根就是区间 $[l,r]$ 中 dfn 最小的结点和 dfn 最大的结点的 lca。

step2

难点在于第一部分。

现在来分析右端点固定为 $r$ 时,对于一个结点 $u$,左端点 $l$ 取到何值会让 $u$ 对第一部分有贡献。记 $mx_u$ 表示 $u$ 的子树中在前缀 $[1,r]$ 中最后一次出现时间最大的点的最后一次出现位置,容易发现当 $l \in [1,mx_u]$ 时,$u$ 子树中的点 $a_{mx_u}$ 必然为黑点,当 $l \in [mx_u + 1, r]$ 时,$u$ 子树中必然不存在黑点;所以 $u$ 会且仅会对区间 $[1,mx_u]$ 有 $+1$ 的贡献。

考虑扫描线:枚举 $r$,动态维护 $mx_u$ 以及所有左端点的答案,初始有 $mx_u = 0$。观察从 $r-1$ 枚举到 $r$ 时 $a_r$ 会对 $mx_u$ 造成什么影响:就是将 $a_r$ 到 $1$ 的路径上的所有点的 $mx_u$ 赋值为 $i$ 。这很容易联想到 LCT 的 access 操作,也就是说,如果暴力对于从 $a_r$ 到 $1$ 的路径上的 $mx_u$ 相同的连续段分别修改的话,根据 access 的复杂度分析,这是均摊单次 $O(\log n)$ 的(可以看作每次操作都是在 $O(\log n)$ 条重链上做颜色段均摊)。而在整条链 $mx_u$ 相同的情况下,我们只需要知道这条链的点数即可用 BIT 快速修改。

这个过程用 LCT 即可非常方便地模拟,总复杂度 $O(n \log^2 n)$。

代码:

\/\/区间虚树
#include <bits\/stdc++.h>
#define lowbit(x) (x & -x)
#define eb emplace_back
#define pb push_back
#define mp make_pair
using namespace std;

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

typedef long long ll;
const int N = 1e5+5;

int n, m, q, c[N], ans[N];
int siz[N], dep[N], hs[N], f[N];
int ct[N], dfn[N], nfd[N];
int mn[N][17], mx[N][17], lg2[N];

vector<int> T[N];
vector<pair<int, int> > Q[N];

void dfs1(int x, int fa) {
	f[x] = fa;
	dep[x] = dep[fa] + 1;
	siz[x] = 1;
	for (auto son : T[x]) {
		if (son == fa) continue;
		dfs1(son, x);
		siz[x] += siz[son];
		if (siz[son] > siz[hs[x]]) hs[x] = son;
	}
}

void dfs2(int x, int ctop) {
	ct[x] = ctop;
	nfd[dfn[x] = ++dfn[0]] = x;
	if (hs[x]) dfs2(hs[x], ctop);
	for (auto son : T[x]) {
		if (son == f[x] || son == hs[x]) continue;
		dfs2(son, son);
	}
}

inline int LCA(int x, int y) {
	while (ct[x] != ct[y]) {
		if (dep[ct[x]] < dep[ct[y]]) swap(x, y);
		x = f[ct[x]];
	}
	return dep[x] < dep[y] ? x : y;
}

int BIT[N];
inline void upd(int x, int k) { for (; x <= m; x += lowbit(x)) BIT[x] += k; }
inline void upd(int l, int r, int k) { upd(l, k), upd(r + 1, -k); }
inline int qry(int x) { int ret = 0; for (; x; x -= lowbit(x)) ret += BIT[x]; return ret; } 

namespace LCT {
	struct node {
		int son[2], fa;
		int cov, col;
	} T[N];

	#define son(x, s) T[x].son[s]
	#define f(x) T[x].fa

	inline void cov(int x, int k) { if (x) T[x].cov = T[x].col = k; }
	inline void push_down(int x) { if (int &t = T[x].cov) cov(son(x, 0), t), cov(son(x, 1), t), t = 0; }
	inline void cct(int x, int y, bool loc) { son(x, loc) = y, f(y) = x; }
	inline bool get_son(int x) { return son(f(x), 1) == x; }
	inline bool isRt(int x) { return son(f(x), 0) != x && son(f(x), 1) != x; }

	inline void rotate(int x) {
		int y = f(x);
		bool loc = get_son(x);
		f(x) = f(y);
		if (!isRt(y)) {
			son(f(y), get_son(y)) = x;
		}
		cct(y, son(x, loc ^ 1), loc);
		cct(x, y, loc ^ 1);
	}

	int stk[N], top;

	inline void splay(int x) {
		int y = x;
		stk[top = 1] = y;
		while (!isRt(y)) stk[++top] = y = f(y);
		while (top) push_down(stk[top--]);
		for (y = f(x); !isRt(x); rotate(x), y = f(x))
			if (!isRt(y))
				rotate(get_son(x) == get_son(y) ? y : x);
	}

	inline void access(int x, int t) {
		int y = 0;
		for (; x; x = f(y = x)) {
			if (y) upd(T[y].col + 1, t, dep[y] - dep[x]);
			splay(x), son(x, 1) = y;
		}
		upd(T[y].col + 1, t, dep[y]);
		cov(y, t);
	}

	#undef son
	#undef f
}

int main() {
	n = read(), m = read(), q = read();
	for (int i = 1; i < n; i++) {
		int u = read(), v = read();
		T[u].pb(v);
		T[v].pb(u);
	}

	dfs1(1, 0);
	dfs2(1, 1);
	for (int i = 2; i <= n; i++) LCT::T[i].fa = f[i];
	
	lg2[0] = -1;
	for (int i = 1; i <= m; i++) {
		c[i] = read();
		lg2[i] = lg2[i >> 1] + 1;
		mn[i][0] = mx[i][0] = dfn[c[i]];
		for (int j = 1; j <= lg2[i]; j++) {
			mn[i][j] = min(mn[i][j - 1], mn[i - (1 << j - 1)][j - 1]);
			mx[i][j] = max(mx[i][j - 1], mx[i - (1 << j - 1)][j - 1]);
		}
	}

	for (int i = 1; i <= q; i++) {
		int l = read(), r = read();
		Q[r].eb(l, i);
	}
	
	for (int i = 1; i <= m; i++) {
		LCT::access(c[i], i);
		for (auto j : Q[i]) {
			int L = j.first, R = i, len = lg2[R - L + 1];
			ans[j.second] = qry(L);
			int mnd = min(mn[R][len], mn[L + (1 << len) - 1][len]);
			int mxd = max(mx[R][len], mx[L + (1 << len) - 1][len]);
			ans[j.second] -= dep[LCA(nfd[mnd], nfd[mxd])] - 1;
		}
	}

	for (int i = 1; i <= q; i++) printf("%d\n", ans[i]);
	return 0;
}

APIO2023 T2 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2023-05-23 17:43:29

题目大意:

给定序列 $a_{1, \dots, n}$,定义 $S_{l,r}$ 为 $a_{l,\dots,r}$ 的中位数集合(大小为 $1$ 或 $2$),$w_{l,r,x} = \sum_{i=l}^r [a_i=x]$,求 $\max_{1 \le l \le r \le n} \max_{x \in S_{l,r}} w(l,r,x)$。

$n \le 5 \times 10^5$。


小清新数据结构。

容易想到枚举中位数的值,设为 $x$,将序列中 $<x$ 的位置赋为 $0$,$>x$ 的位置赋为 $1$,目标即为计算所有 $x$ 是中位数的区间中 $x$ 的最大出现次数。

step1

先来考虑 $x \in S_{l,r}$ 的等价条件是什么,设区间中 $x$ 的出现次数为 $c$,$0$ 的出现次数为 $c_0$,$1$ 的出现次数为 $c_1$,不难得到:

$$ \begin{aligned} &x \in S_{l,r}\

\Leftrightarrow &\begin{cases} c+c_0 \ge c_1\ c+c_1 \ge c_0\ \end{cases}\

\Leftrightarrow &\max(c_1-c_0-c,c_0-c_1-c) \le 0

\end{aligned} $$

设 $u_{i} = w(1,i,1)-w(1,i,0)-w(1,i,x)$,$v_i = w(1,i,0)-w(1,i,1)-w(1,i,x)$,那么 $x \in S_{l,r}$ 当且仅当 $\max(u_r-u_{l-1}, v_r - v_{l-1}) \le 0$,将 $p_i=(u_i, v_i)$ 看作二维平面上的一个点,这个条件实际上就是在说 $p_r$ 在 $p_{l-1}$ 的左下方。

step2

现在再来观察 $p_{i-1}$ 和 $p_i$ 之间的位置关系。如果 $a_i = x$,那么 $p_{i}$ 是 $p_{i-1}$ 向左下走一步;如果 $a_i = 0$,那么是向左上走一步;如果 $a_i=1$,那么是向右下走一步。画出来会像下图:

画出来就能一眼看出这张图的特点:由若干条(准确来说是 $w(1,n,x)+1$ 条)斜率为 $-1$ 的线段以及连接它们的斜率为 $1$ 的线段组成。现在我们的目标就变成了找到相距最远的两条斜率为 $-1$ 线段(距离定义为从一条走到另一条需要向左下走的次数),满足能够从其上面分别找出一个点满足一个在另一个的左下方。

考虑能够找出这样两个点的等价条件是什么,不妨来看上图中的线段 BC 和 DE,画一下图就能得出条件:

$$ \begin{cases} C_x \ge D_x\ B_y \ge E_y\ \end{cases} $$

二维数点的形式。使用 BIT,支持单点修改,后缀 $\min$ 即可;第 $i$ 条斜率为 $-1$ 的线段实际上就是点 $x$ 的第 $i-1$ 次出现位置到第 $i$ 次出现位置之间的所有位置对应的点的集合,所以线段端点的横纵坐标只需要使用线段树,支持区间加,区间 $\max \And \min$ 即可。

总复杂度 $O(n \log n)$。

代码:

\/\/#include "sequence.h"
#include <bits\/stdc++.h>
#define lowbit(x) (x & -x)
#define eb emplace_back
#define pb push_back
#define mp make_pair
using namespace std;

#define getchar() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
char buf[1 << 21], *p1 = buf, *p2 = buf;

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 - '0', c = getchar();
    return x * f;
}

typedef long long ll;
const int N = 5e5+5;
const int Mod = 998244353;
const int INF = 0x3f3f3f3f;

int n, a[N], ans;
vector<int> pos[N];

struct segT {
    struct node {
        int mx, mn, ad;
    } T[N << 2];

    #define ls (p << 1)
    #define rs (p << 1 | 1)

    inline void push_up(int p) {
        T[p].mx = max(T[ls].mx, T[rs].mx) + T[p].ad;
        T[p].mn = min(T[ls].mn, T[rs].mn) + T[p].ad;
    }

    void upd(int p, int l, int r, int gl, int gr, int k) {
        if (l >= gl && r <= gr) {
            T[p].mx += k, T[p].mn += k;
            T[p].ad += k;
            return;
        }
        int mid = (l + r) >> 1;
        if (mid >= gl) upd(ls, l, mid, gl, gr, k);
        if (mid < gr) upd(rs, mid + 1, r, gl, gr, k);
        push_up(p);
    }

    int qry_mn(int p, int l, int r, int gl, int gr) {
        if (l >= gl && r <= gr) return T[p].mn;
        int mid = (l + r) >> 1, ret = INF;
        if (mid >= gl) ret = qry_mn(ls, l, mid, gl, gr);
        if (mid < gr) ret = min(ret, qry_mn(rs, mid + 1, r, gl, gr));
        return ret + T[p].ad;
    }

    int qry_mx(int p, int l, int r, int gl, int gr) {
        if (l >= gl && r <= gr) return T[p].mx;
        int mid = (l + r) >> 1, ret = -INF;
        if (mid >= gl) ret = qry_mx(ls, l, mid, gl, gr);
        if (mid < gr) ret = max(ret, qry_mx(rs, mid + 1, r, gl, gr));
        return ret + T[p].ad;
    }

    #undef ls
    #undef rs
} Tx, Ty;

int cnt;

struct opt {
    int op, x, y, k;\/\/0询问,1修改
} P[N * 2];

struct Hsh {
    int v[N * 2], c;
    int operator [](int x) { return v[x]; }
    inline void ins(int x) { v[++c] = x; }
    inline void init() {
        sort(v + 1, v + c + 1);
        c = unique(v + 1, v + c + 1) - v - 1;
    }
    inline int f(int x) { return lower_bound(v + 1, v + c + 1, x) - v; }
} H;

struct BIT {
    int T[N * 2];
    inline void upd(int x, int k) { for (; x; x -= lowbit(x)) T[x] = min(T[x], k); }
    inline int qry(int x) { int ret = INF; for (; x <= H.c; x += lowbit(x)) ret = min(ret, T[x]); return ret; }
    inline void clr(int x) { for (; x; x -= lowbit(x)) T[x] = INF; }
} T;

inline void work() {\/\/二维数点
    sort(P + 1, P + cnt + 1, [](opt a, opt b) { return a.x == b.x ? a.op > b.op : a.x > b.x; });

    H.c = 0;
    for (int i = 1; i <= cnt; i++) H.ins(P[i].y);
    H.init();

    for (int i = 1; i <= cnt; i++) {
        P[i].y = H.f(P[i].y);
        if (P[i].op == 1) T.upd(P[i].y, P[i].k);
        else ans = max(ans, P[i].k - T.qry(P[i].y));
    }
    for (int i = 1; i <= cnt; i++) T.clr(P[i].y);
}

int sequence(int N, std::vector<int> A) {
	n = N;
	for (int i = 1; i <= n; i++) pos[A[i - 1]].pb(i);
	
	memset(T.T, 0x3f, sizeof(T.T));
    for (int i = 1; i <= n; i++) Tx.upd(1, 0, n, i, i, i), Ty.upd(1, 0, n, i, i, -i);
    for (int i = 1; i <= n; i++) if (!pos[i].empty()) {
        for (int j : pos[i])\/\/from c1 to c
            Tx.upd(1, 0, n, j, n, -2);

        cnt = 0;
        pos[i].pb(n + 1);
        int k = pos[i].size(), lst = 0;
        for (int j = 0; j < k; j++) {
            int cur = pos[i][j];
            int mn_x = Tx.qry_mn(1, 0, n, lst, cur - 1), mn_y = Ty.qry_mn(1, 0, n, lst, cur - 1);
            int mx_x = Tx.qry_mx(1, 0, n, lst, cur - 1), mx_y = Ty.qry_mx(1, 0, n, lst, cur - 1);
            P[++cnt] = (opt) { 1, mx_x, mx_y, j };
            P[++cnt] = (opt) { 0, mn_x, mn_y, j };
            lst = cur;
        }

        work();

        pos[i].pop_back();
        for (int j : pos[i])\/\/from c to c0
            Ty.upd(1, 0, n, j, n, 2);
    }
    
    return ans;
}

\/\/int main() {
\/\/    int n;
\/\/    vector<int> A;
\/\/	scanf("%d", &n); A.resize(n);
\/\/	for (int i = 0; i < n; i++) scanf("%d", &A[i]);
\/\/	
\/\/	printf("%d", sequence(n, A));
\/\/    return 0;
\/\/}

JOISC 2023 D1T2 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2023-07-02 23:36:49

传送门: P9330


牛逼 dp。

step1 $(n \le 8)$:

首先真贪心是把区间按照右端点排序,然后从前到后能选就选。

我们考虑求答案的补,即计算假贪心能够求出正确答案的方案数,总方案数为 $1-2n$ 中所有奇数之积,所以当 $n \le 8$ 时直接 dfs 即可。

总方案数的证明考虑 $i$ 从 $1$ 至 $n$ 安排 $a_i,b_i$:

  • 当安排 $a_i$ 时方案数为 $1$,只能安排最小的未被选过的数。
  • 当安排 $b_i$ 时方案数为 $2n-2i+1$,可以安排任意一个未被选过的数。

step2 $(n \le 300)$:

将真贪心选中的区间称为区间,假贪心选中的区间称为区间,注意两种贪心可能选中同一个区间,不妨将这种区间称为区间,其它区间则为区间。

那么一组红区间应当满足的性质为:

  • 相邻两个红区间无交。
  • 两个相邻红区间的右端点之间不存在其它完整区间,特别的,第一个红区间的右端点之前不存在其它完整区间

同时也容易证明当一组区间满足上述条件时,这组区间也一定是真贪心选出来的红区间。

同样可以得出蓝区间的性质:

  • 相邻两个蓝区间无交。
  • 前一个蓝区间的右端点和后一个蓝区间的左端点之间不存在其它区间的左端点,特别的,第一个蓝区间的左端点之前不存在其他区间的左端点

和红区间一样,满足上述性质的一组区间也一定是假贪心选出的蓝区间。

我们再将红蓝区间放到一起观察,由于要求红蓝区间个数相等,所以我们将第 $i$ 个红区间与第 $i$ 个蓝区间配对,可以发现第 $i$ 个红区间的右端点一定大于第 $i-1$ 个蓝区间的右端点,小于等于第 $i$ 个蓝区间的右端点。

配合下图理解(截自日文题解):

dp 的最重要一步来了:考虑按顺序每次插入一对红蓝区间

具体来说,在 dp 状态中记录当前已经插入的区间个数 $i$,转移就考虑每次插入“一对新的红蓝区间以及所有右端点在当前最后一个红区间右端点和新插入的红区间右端点之间的黑区间”,所以我们需要枚举新插入的黑区间个数 $j$;同时我们还需要为这些新插入的黑区间右端点分配左端点,而左端点还有关于蓝区间的限制(见上文),所以还需要在状态中记录之前能插入一个左端点的位置数 $k$;同时还要添一维 $0/1$ 表示当前这对红蓝区间是不是紫区间。

转移细节就不展开写了,不是重点。

于是我们就得到了状态 $O(n^2)$,转移 $O(n)$ 的 $O(n^3)$ dp。

step3 $(n \le 3000)$:

按照插入区间的思路,转移感觉上就是不可避免地需要枚举插入的黑区间个数,所以我们考虑优化状态数。

观察上述的 $O(n^2)$ 状态的 dp,注意到需要多出来一维 $k$ 是因为左端点有较强的限制,但是右端点的限制较弱,于是我们就可以考虑去转换左右端点

具体来说,我们考虑倒着插入每一对红蓝区间,枚举新插入的黑区间数量 $j$,同时为新插入的黑区间左端点分配右端点,这时就会发现右端点是可以插入到之前任意两个端点之间的

于是我们可以设计 dp:$f_{i,0/1}$ 表示当前已经插入了 $i$ 个区间,当前这对红蓝区间是(1)否(0)是紫区间,根据 $0/1$ 不同一共有 $4$ 种转移式,这里具体展开一下 $0 \rightarrow 0$ 的转移式:

$$ f_{i+j+2,0} \leftarrow f_{i+j+2,0} + f_{i,0} \cdot (j+2)(j+1) \cdot (2i-2+(j-1))^{\underline{j}} $$

其中 $(2i-2+(j-1))^{\underline{j}}$ 为插入 $j$ 个黑区间右端点的方案数,$(j+2)(j+1)$ 为将共计 $j+3$ 个端点划分为 $3$ 段的方案数,配合下两图理解:

其它 $3$ 种转移大同小异,请自行画图理解。

于是我们得到了状态 $O(n)$,转移 $O(n)$ 的 $O(n^2)$ dp,同时也存在其它 $O(n^2)$ dp 方法,但大多由于为状态 $O(n^2)$,转移 $O(1)$ 而无法进一步优化。

$O(n^2)$ 核心代码:

f[1][0] = f[2][1] = 1;
	for (int i = 1; i < n; i++)
		for (int p = 0; p < 2; p++) if (f[i][p])
			for (int j = 0; i + j + 1 <= n; j++)
				for (int q = 0; i + j + 1 + q <= n && q < 2; q++) {
					ll t = f[i][p];
					if (q) t = t * (j + 1 + p) % Mod;
					if (p) t = t * (j + 1) % Mod;
					Add(f[i + j + 1 + q][q], t * fac[2 * i - 3 + !p + j] % Mod * ifac[2 * i - 3 + !p] % Mod);
				}

step4 $(n \le 20000)$:

其实 n 方卡卡常就能草过去。

依然以 $0 \rightarrow 0$ 的转移为例(其它大同小异),令 $j \leftarrow i+j+2$,重新化简一下转移式:

$$ \begin{aligned} f_{j,0} \leftarrow f_{j,0} &+f_{i,0} \cdot (j-i)(j-i-1) \cdot \dfrac{(j+i-5)!}{(2i-3)!}\ f_{j,0} \leftarrow f_{j,0} &+ \dfrac{f_{i,0}(i^2+i)}{(2i-3)!} \cdot (j+i-5)!\ &+ (j^2-j)\dfrac{f_{i,0}}{(2i-3)!}(j+i-5)!\ &- 2j\dfrac{f_{i,0}i}{(2i-3)!}(j+i-5)! \end{aligned} $$

式子中只有和 $i,j,i+j$ 有关的项,所以这实际上就是半在线减法卷积,使用分治法解决即可。

注意到是任意模数,写 NTT 巨烦,可以考虑用 Karatsuba 乘法代替。

使用 NTT 或 Karatsuba 乘法即可得到 $O(n \log^2 n)$ 或 $O(n^{1.59})$ 的复杂度。

细节较多,注意代码实现。

代码(使用 Karatsuba 乘法):

#include <bits\/stdc++.h>
#define lowbit(x) (x & -x)
#define eb emplace_back
#define pb push_back
#define mp make_pair
using namespace std;

typedef long long ll;
typedef vector<int> poly;
const int N = 4e4+5;

struct FastMod {
    ll p, m;
    FastMod(ll pp): p(pp), m((1ll << 62) \/ pp) { }
    ll operator ()(ll x) {
        ll r = x - p * ((__int128) x * m >> 62);
        return r >= p ? r - p : r;
    }
} F(2);

int n, Mod, ans = 1;
int f[N][2];
ll Bfac[N], Bifac[N], *fac = Bfac + 2, *ifac = Bifac + 2;

inline ll qpow(int a, int b) {
	ll base = a, ans = 1;
	while (b) {
		if (b & 1) ans = ans * base % Mod;
		base = base * base % Mod;
		b >>= 1;
	}
	return ans;
}

inline void Add(int &a, int b) { a += b; if (a >= Mod) a -= Mod; }
inline void Add(ll &a, int b) { a += b; if (a >= Mod) a -= Mod; }

ll A[N], B[N], C[N], r0[15][N], r1[15][N], r2[15][N], x[15][N], y[15][N];

inline int mul(int d, ll *a, ll *b, int n, ll *g) {
    if (!n) return 0;
    for (int i = 0; i < 2 * n - 1; i++) g[i] = 0;
    \/\/ if (1) {
    if (n <= 16) {
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                g[i + j] = F(g[i + j] + a[i] * b[j]);
        return 2 * n - 1;
    }
    int mid = (n + 1) \/ 2;
    int l0 = mul(d + 1, a, b, mid, r0[d]);
    int l1 = mul(d + 1, a + mid, b + mid, n - mid, r1[d]);
    for (int i = 0; i < l0; i++) Add(g[i + mid], Mod - r0[d][i]), Add(g[i], r0[d][i]);
    for (int i = 0; i < l1; i++) Add(g[i + mid], Mod - r1[d][i]), Add(g[i + 2 * mid], r1[d][i]);

    for (int i = 0; i < mid; i++) {
        x[d][i] = a[i], y[d][i] = b[i];
        if (mid + i < n) Add(x[d][i], a[mid + i]), Add(y[d][i], b[mid + i]);
    }
    int l2 = mul(d + 1, x[d], y[d], mid, r2[d]);

    for (int i = 0; i < l2; i++) Add(g[i + mid], r2[d][i]);

    return 2 * n - 1;
}

inline ll c0(int p, int q, ll j) { return F(p * q * j * j + (p + q - p * q) * j); }
inline ll c1(int p, int q, ll i) { return F(p * q * i * i + (p * q - p - q) * i + (1 - p) * (1 - q) + Mod); }

void solve(int l, int r) {
	if (l == r) return;
	int mid = (l + r) >> 1;
	solve(l, mid);
	for (int p = 0; p < 2; p++)
		for (int q = 0; q < 2; q++) {
			for (int i = 0; i < r - l; i++) B[i] = fac[mid - 2 + l - q + i - p], A[i] = 0;
			if (p * q || (p + q - p * q)) {
				for (int i = l; i <= mid; i++) A[mid - i] = F(f[i][p] * ifac[2 * i - 2 - p]);
				mul(0, A, B, r - l, C);
				for (int i = mid - l; i <= r - l - 1; i++) {
					int j = i + 1 + l - q;
                    f[j + q][q] = F(f[j + q][q] + c0(p, q, j) * C[i]);
				}
			}

			if (p * q || (p * q - p - q) || (1 - p) * (1 - q)) {
				for (int i = l; i <= mid; i++) A[mid - i] = F(F(f[i][p] * ifac[2 * i - 2 - p]) * c1(p, q, i));
				mul(0, A, B, r - l, C);
				for (int i = mid - l; i <= r - l - 1; i++) Add(f[i + 1 + l][q], C[i]);
			}

			if (p * q) {
				for (int i = l; i <= mid; i++) A[mid - i] = F(F(f[i][p] * ifac[2 * i - 2 - p]) * i);
				mul(0, A, B, r - l, C);
				for (int i = mid - l; i <= r - l - 1; i++) {
					int j = i + 1 + l - q;
					Add(f[j + q][q], Mod - F(C[i] * 2 * j));
				}
			}
		}
	solve(mid + 1, r);
}

int main() {
	scanf("%d%d", &n, &Mod); F = FastMod(Mod);

	fac[0] = 1;
	for (int i = 1; i <= 2 * n; i++) fac[i] = fac[i - 1] * i % Mod;
	ifac[2 * n] = qpow(fac[2 * n], Mod - 2);
	for (int i = 2 * n; i; i--) ifac[i - 1] = ifac[i] * i % Mod;

	f[1][0] = f[2][1] = 1;
	solve(1, n);

	for (int i = 3; i <= 2 * n; i += 2) ans = (ll) ans * i % Mod;
	for (int i = 1; i <= n; i++)
		for (int p = 0; p < 2; p++) {
			ll t = f[i][p];
			if (p) t = t * (n - i + 1) % Mod;
			Add(ans, Mod - t * fac[n + i - 3 + !p] % Mod * ifac[2 * i - 3 + !p] % Mod);
		}

	printf("%d", ans);
	return 0;
}

P8352 SDOI2022 D2T1 小 N 的独立集 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2022-05-18 09:46:31

传送门:P8352 [SDOI/SXOI2022] 小 N 的独立集


题意

给定 $n$ 个点的树的形态以及点的权值范围 $k$,对于任意 $i \in [1,kn]$, 求有多少种权值分配方案,使得树的最大权独立集大小为 $i$。


1. 经典 dp $(n \le 8)$:

枚举所有权值分配方案,设 $f_{u,0/1}$ 表示 $u$ 子树中点 $u$ 不选/选的最大独立集然后做经典树上最大权独立集 dp 即可,复杂度 $O(nk^n)$。

2. dp 套 dp $(n \le 100)$:

经典 dp 和极小的值域 $(k \le 5)$ 启发我们可以将 $f_{u,0/1}$ 的值设为状态

设 $g_{u,v0,v1}$ 为 $u$ 子树中 $f_{u,0},f_{u,1}$ 分别为 $v0,v1$ 时的方案数。转移时考虑将 $u$ 的一个儿子 $v$ 合并到 $u$ 中,枚举 $i,j,p,q$ 分别作为 $f_{u,0},f_{u,1},f_{v,0},f_{v,1}$,易得转移:

$$g'{u,i+\max(p,q),j+p} \gets g'{u,i+\max(p,q),j+p}+g_{u,i,j} \times g_{v,p,q}$$

套用树上背包的复杂度分析可得时间复杂度为 $O(n^4k^4)$,加上判0远远跑不满,能够通过 $n \le 100$。

因为状态转移时是将内层 $f$ 的转移结果作为外层 $g$ 的状态来转移的,所以称作 dp 套 dp。

3. 状态优化 $(n \le 1000)$:

上文的 dp 方法复杂度瓶颈在于状态数就已经到达了 $O(n^3k^2)$ 级别,此时可以考虑简化内层 $f$ 的状态和转移,考虑另一种树上最大权独立集的 dp 方法:重新定义 $f_{u,0/1}$ 表示 $u$ 子树内是(1)否(0)强制不选节点 $u$ 时的最大方案大小,转移显然,如此设便得到了一条重要性质:$0 \le f_{u,0}-f_{u,1} \le val_{u} \le k$(强制不选的答案肯定小于等于无限制的答案,且两者一定不会差出 $u$ 点的权值),此时自然得出 $g_{u,v1,d}$ 表示 $u$ 子树中 $f_{u,0},f_{u,1}$ 分别为 $v1+d,v1$ 时的方案数,依然枚举 $i,j,p,q$,有转移:

$$g'{u,i+p+q,\max(i+j+p,i+p+q)-(i+p+q)} \gets g'{u,i+p+q,\max(i+j+p,i+p+q)-(i+p+q)}+g_{u,i,j} \times g_{v,p,q}$$

时间复杂度$O(n^2k^4)$,加判0跑得很快

代码(代码中的 $f$ 数组即为 $g$):

\/\/dp套dp
#include <bits\/stdc++.h>
using namespace std;

const int MAXN = 1001;
const int Mod = 1e9+7;

int n, k, siz[MAXN];
int f[MAXN][MAXN * 5][6], t[MAXN * 5][6];
int head[MAXN], edge_num;

struct edge {
    int nxt, to;
} T[MAXN * 2];

inline void add(int u, int v) {
    T[++edge_num] = (edge) {head[u], v};
    head[u] = edge_num;
}

inline void Add(int &a, int b) { a += b; if (a >= Mod) a -= Mod; }

void dfs(int x, int fa) {
    siz[x] = 1;
    for (int i = 1; i <= k; i++) f[x][0][i] = 1;
    for (int i = head[x]; i; i = T[i].nxt) {
        int son = T[i].to;
        if (son == fa) continue;
        dfs(son, x);
        memset(t, 0, sizeof(t));
        for (int i = 0; i <= k * siz[x]; i++)
            for (int j = 0; j <= k; j++) if (f[x][i][j])
                for (int p = 0; p <= k * siz[son]; p++)
                    for (int q = 0; q <= k; q++) if (f[son][p][q])
                        Add(t[i + p + q][max(i + j + p, i + p + q) - (i + p + q)], 1ll * f[x][i][j] * f[son][p][q] % Mod);
        memcpy(f[x], t, sizeof(t));
        siz[x] += siz[son];
    }
}

int main() {
    scanf("%d%d", &n, &k);
    for (int i = 1; i < n; i++) {
        int u, v;
        scanf("%d%d", &u, &v);
        add(u, v), add(v, u);
    }

    dfs(1, 0);

    for (int i = 1; i <= k * n; i++) {
        int ans = 0;
        for (int d = 0; d <= min(i, k); d++) Add(ans, f[1][i - d][d]);
        printf("%d\n", ans);
    }
    return 0;
}

[数据结构] 线段树历史信息维护

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2022-05-30 09:56:29

感觉线段树维护历史信息的题做完就忘 /yun……于是开篇文章记录下

本文认为 $n,m$ 同阶,$mx,hmx,sum,hs,,A,t$ 分别为区间最大值、历史最大值、和、历史和、加法操作、累加操作(具体意义下文还会解释)。


主要思路

线段树的维护需要考虑三个方面

  • 信息与信息合并
  • 信息与标记合并
  • 标记与标记合并

其中第一点就是 push_up 函数,在这类题中一般都是平凡的,不再赘述。

关键在于最后两点,将每个节点上覆盖的按时间排序的操作序列提取出来,我们的目标就是将这一段 $O(n)$ 长度的操作序列用 $O(1)$ 个 tag 描述出来(其实就是懒标记),并且能做到正确地合并两个节点的操作序列以及对节点信息的更新,也就是 push_down 函数。


1. 历史最值

先来一道练手题:SP2916

一道经典的区间非空最大子段和的一眼题,线段树节点维护最大前后缀以及最大子段和即可。

不过这里我想提出来另一种非空最大子段和做法:区间加,区间维护历史最大值。我才不会说我是因为脑梗没看出来维护最大子段和所以用维护历史最值方法做的

将序列做前缀和,不难发现询问 $[l,r]$ 就是求 $\max_{l \le i < j \le r}(sum_{j} - sum_{i})$,将右端点作为横轴,左端点作为纵轴,那么坐标系中每一个点 $(x,y)$ 就对应了一个原序列的一个子段 $[x,y]$,将该点的的权值设为 $sum_{y}-sum_{x-1}$ (如果 $x>y$ 则为 $-\inf$),那么对于一个询问,我们求的就是下图中的红色三角形部分的最大值,因为有 $-\inf$, 所以直接扫描线,区间加维护扫描线上的值,求区间历史最大值即可,下面具体讲解一下如何维护:

首先肯定要对每个线段树节点维护区间最值和区间历史最值两个值,然后考虑怎样描述一个节点上的操作序列,假设当前节点有操作序列:

$$A_{1},A_{2},A_{3}...A_{k}$$

假如我们只用序列的和来描述的话,当我们下传该序列接到儿子的序列之后时就会发现我们无法更新儿子的历史最大值了,因为实际上可能是在某一时刻 $t(t<k)$而不是在 $k$,$mx+{\textstyle \sum_{i=1}^{t}}A_{i}$ 取得了最大值,导致 $hmx$ 偏小。

那么此时正解就十分显然了,除了维护 $\sum A_{i}$,我们再维护一个前缀最大值 $hadd$,push_down 时执行 $hmx \gets \max(hmx,mx+hadd)$ 即可正确维护。

代码(实现上这种类型只需要改一下 push_down 即可)

习题(有题再加):


2、历史版本和

考虑加上区间累加操作 $t$,表示将 $sum$ 累加到 $hs$ 上 $t$ 次,对于一个操作序列(相邻同类型操作已经合并):

$$A_{1},t_{1},A_{2},t_{2},A_{3},t_{3}...$$

考虑用 $sa,st$ (分别表示 $\sum A_{i}, \sum t_{i}$)描述这个序列,下放时 $sum \gets sum+len \times sa$、$hs \gets hs+sum \times st$。 但这样显然会多算,比如 $A_{3}$ 多算了 $t_{1} + t_{2}$ 次,这时候再维护一个 tag $hadd$ 用来表示多算的贡献,每次向序列后加入一个 $A$ 时,$hadd \gets hadd + st \times A$ 即可。

习题(有题再加):

  • P3246 [HNOI2016]序列 (好像题解区还没有历史版本和的解法,这里给出我的代码好像只要差分一下就可以了,我又双叒叕想麻烦了
  • CF997E Good Subsegments (扫描线技巧,区间加维护最小值0历史出现次数,需要仔细观察性质)
  • 这个也少QAQ

语言没有怎么仔细打磨,哪里讲得不清楚还请指出QAQ,反正未来的我能看懂就行(逃

[多项式] 生成函数题目集

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2022-06-03 20:47:31

搞一个生成函数计数题目集,,,不定期更新


P3978 [TJOI2015]概率论

题解区有更优美的通项解释方法,然而我只会暴力推式子

设 $f_i,g_i$ 分别表示 $i$ 个点的二叉树形态个数及其叶子总数量,$F(x),G(x)$ 为它们的 $\operatorname {OGF}$。

经典枚举左子树大小,有:

$$ \begin{aligned} f_i &= \sum_{j=0}^{i-1}f_jf_{i-1-j}+[i = 0]\ F(x) &= \sum_{i=0}^{+ \infty} ( \sum_{j=0}^{i-1}f_jf_{i-1-j}) x^i+1\ &= xF^2(x)+1\ F(x) &= \frac{1 \pm \sqrt{1-4x}}{2x} \end{aligned} $$

显然当 $\lim_{x \to 0}$ 时 取加号会导致 $F(x) \to +\infty$,故 $F(x)= \dfrac{1- \sqrt{1-4x}}{2x}$,实际上这就是卡特兰数的 $\operatorname{OGF}$。

再看 $g_i$,依然枚举左子树的大小:

$$ \begin{aligned} g_i &= 2 \sum_{j=0}^{i-1} f_jg_{i-1-j}+[i=1]\ G(x) &= 2 \sum_{i=0}^{+ \infty} ( \sum_{j=0}^{i-1}f_jg_{i-1-j})x^i+x\ &= 2xF(x)G(x)+x\ G(x) &= \frac{x}{1-2xF(x)}\ &= x (1-4x)^{- \frac{1}{2}}\ &= x \sum_{i=0}^{+ \infty} \binom{- \frac{1}{2}}{i}(-4x)^i\ &= \sum_{i=0}^{+ \infty} \binom{2i}{i}x^{i+1} \end{aligned} $$

最后的推导用到了牛顿二项式定理。
于是我们得到 $g_i=\binom{2i-2}{i-1}$,$ans=\dfrac{g_n}{f_n}=\dfrac{n^2+n}{4n-2}$ .


CF438E The Child and Binary Tree

一道经典题目。

由于节点数没有限制,直接设 $f_i$ 为权值为 $i$ 时的树的个数,再设 $g_i=[i \in \{c_{1...n}\}]$,$F(x),G(x)$ 分别为它俩的 $\operatorname {OGF}$。
话不多说,暴力列式子硬刚就完了:

$$ \begin{aligned} f_i &= \sum_{j=0}^{i}g_{i-j} \sum_{k=0}^{j}f_kf_{j-k}+[i=0]\ F(x) &= \sum_{i=0}^{+ \infty} (\sum_{j=0}^{i}g_{i-j} \sum_{k=0}^{j}f_kf_{j-k})x^i+1\ &= G(x)F^2(x)+1\ \end{aligned} $$

我们得到:

$$G(x)F^2(x)-F(x)+1 = 0$$

到这里,我们有两种做法:

$1)$ 无脑牛顿迭代

$$F \equiv F_0-\frac{GF_0^2-F_0+1}{2GF_0-1}$$

求出至少模 $x^{m+1}$ 意义下的答案即可,复杂度 $O(n \log n)$,没什么好说的。

$2)$ 直接解二次方程

$$F=\frac{1 \pm \sqrt{1-4G}}{2G}$$

验证 $\lim_{x \to 0}$ 的情况可以舍去加号,即 $F=\dfrac{1 - \sqrt{1-4G}}{2G}$ .
但是注意到 $[x^0]G(x)$ 一定等于 $0$, 似乎没办法求逆了……吗?

$$ \begin{aligned} F &= \frac{1 - \sqrt{1-4G}}{2G}\ &= \frac{2}{1+ \sqrt{1-4G}} \end{aligned} $$

分母无理化,神不神仙我不知道,反正我想不到。
这时候我们猛然发现,分母的常数项变成了 $2$ !结合多项式求根+求逆即可在 $O(n \log n)$ 时间内解决。


射命丸文的笔记

来道 $\operatorname{EGF}$ .

注意到计算点数为 $n$ 的所有竞赛图的总哈密顿回路个数是平凡的,为 $(n-1)! \cdot 2^{\binom{n}{2}-n}$,即选定一个哈密顿回路,计算其在多少个竞赛图中出现。
接下来我们的任务变为了计算出 $n$ 个点的强连通竞赛图数量。

设 $f_i$ 表示 $i$ 个点的强连通竞赛图数量, $g_i=2^{\binom{i}{2}}-[i=0]$,有:

$$ f_i = g_i- \sum_{j=0}^{i} \binom{i}{j} f_jg_{i-j} $$

这是图计数中的常见思路:枚举图的某一特征,具有这一特征的图的计数是平凡的,且每个图有且只有一个特征。比如在经典老番P4841 城市规划中,我们枚举一号点所在的连通块大小;而在本题中,我们可以先缩点,然后枚举拓扑序最小的强连通分量的大小,只要它的大小小于 $i$,那它一定全是出边,其它的点任意连边,得到的一定是不合法的方案总数,总方案数减去即可。

书接上式,辨认出二项卷积形式,应当使用 $\operatorname{EGF}$。设 $F(x),G(x)$ 分别为 $f_i,g_i$ 的 $\operatorname{EGF}$,接着上面的式子:

$$ \begin{aligned} F(x) &= \sum_{i=0}^{+\infty}\frac{g_i- \sum_{j=0}^{i} \binom{i}{j} f_jg_{i-j}}{i!}x^i\ &= G(x)-F(x)G(x)\ F(x) &= \frac{G(x)}{G(x)+1}\ &= 1-(G(x)+1)^{-1} \end{aligned} $$

结合多项式求逆即可在 $O(n \log n)$ 时间内解决。


P5219 无聊的水题 I

$n$ 个点最大度数恰好为 $m$ 的有标号无根树个数。

每次我们取出树中度数为 $1$ 的编号最小的点,删掉并记录唯一一个与其相邻的节点编号,得到的就是这棵树的 $\operatorname {Pr\ddot{u}fer}$ 序列,一个数在 $\operatorname {Pr\ddot{u}fer}$ 序列中出现的次数 $+1$ 就等于其度数,同样,每次我们将当前度数等于 $1$ 的编号最小的点与当前序列的第一个点连边,删掉序列开头并将对应点的当前度数 $-1$,就得到了该 $\operatorname {Pr\ddot{u}fer}$ 序列对应的树。显然,$\operatorname {Pr\ddot{u}fer}$ 序列建立了所有长度为 $n-2$、值域为 $[1,n]$ 的整数序列与所有 $n$ 个点的有标号无根树之间的双射

在该题中,就是多了度数限制,即每个点最多在 $\operatorname {Pr\ddot{u}fer}$ 序列中出现 $m-1$ 次。设 $\operatorname{EGF} F_m(x)= \sum_{i=0}^{m-1}\frac{1}{i!} x^i$,因为要求最大度数恰好为 $m$,所以容斥一下最大度数,答案就是 $x^{n-2}-F_{m-1}^{n-2}(x))$ .

确实挺水

另外关于 $\operatorname {Pr\ddot{u}fer}$ 序列,一个结论是完全图 $K_n$ 的生成树个数为 $n^{n-2}$,更进一步,将 $m$ 个大小分别为 $a_{1...m}(\sum a_i=n)$ 的联通块联通的方案数为(证明见 OI Wiki):

$$n^{m-2}\prod_{i=1}^ma_i$$


P7275 计树

$\operatorname {Pr\ddot{u}fer}$ 序列与容斥结合。

我们将一条编号连续的链称为连续链,观察到一颗树必然可以剖成若干条长度 $\ge 2$ 的极长连续链。结合刚才 $\operatorname {Pr\ddot{u}fer}$ 序列的结论,我们看似有答案:

$$n^{-2}[x^n]\frac{1}{1-\sum_{i\ge 2}nix^i}$$

然而直接这样算会寄掉,原因是会有若干条连续链拼成一整条更长的连续链,这样我们计算的就不是极长连续链了。

考虑给每个长度的连续链配上容斥系数,这里介绍一种机械的容斥系数求法:
首先要明确,一种长度为 $i$ 的连续链的组成方案的容斥系数为参与构成的连续链的容斥系数的乘积,而我们希望的是找到一组系数 $\langle S_i \rangle$,使得所有组成长度为 $i(i\ge 2)$ 的连续段的方案的系数之和为 $1$,其余为 $0$.
所以我们有:

$$\sum_{i\ge 1}S^i=\frac{1}{1-S}-1=\frac{x^2}{1-x}$$

解得 $S=\frac{x^2}{x^2-x+1}$,运用多项式求逆可以得到 $S=\langle 0,0,1,1,0,-1,-1,0,1,1,0,-1,-1,0,...\rangle$ .

所以真正的答案就是

$$n^{-2}[x^n]\frac{1}{1-\sum_{i\ge 2}S_inix^i}$$

结合多项式求逆在 $O(n\log n)$ 时间内解决(杜爷直接用线性递推 $O(\log n)$ 完爆,可是我太蒻了,根本看不出来用线性递推怎么做到 $O(\log n)$)。


P5434 有标号荒漠计数

首先考虑计算 $n$ 个点带标号仙人掌数量。
设 $f_i$ 为强制钦定一个根节点后的数量,这样每个仙人掌会重复计算 $i$ 次,最后除以 $i$ 即可。

如图,对于一个根节点,其可以接入若干个单个的仙人掌,也可以接入若干个仙人掌串。那么我们可以得到 dp 式子:

$$ \begin{aligned} f_i= \sum_{j=0}^{i-1} &\left(\sum_{k=0}^{j} \frac{1}{k!} \sum_{p_1+...+p_k=j,p_x>0} \binom{j}{p_1,...,p_k} \prod_{x=1}^{k}f_{p_x} \right) \cdot\ &\left( \sum_{k=0}^{i-1-j} \frac{1}{k!} \sum_{p1+...+p_k=i-1-j, p_x>0} \binom{i-1-j}{p_1,...,p_k} \prod_{x=1}^{k} \frac{1}{2} \sum_{l=2}^{p_x} \sum_{q_1+...+q_l=p_x, q_y>0} \binom{p_x}{q_1,...,q_l} \prod_{y=1}^{l} f_{q_y} \right) \end{aligned} $$

从中辨认出多项式 $\exp$ 以及多项卷积,选用 $\operatorname{EGF}$,设 $F(x)$ 为 $f_i$ 的 $\operatorname{EGF}$,有:

$$ \begin{aligned} F&=x \cdot \exp F \cdot \exp \left(\frac{1}{2} \sum_{i=2}^{+ \infty} F^i \right)\ &=x \cdot \exp \left(F+\frac{F^2}{2}\frac{1}{1-F} \right)\ &=x \cdot \exp \left(\frac{2F-F^2}{2-2F} \right) \end{aligned} $$

这时候生成函数强大的功能体现得淋漓尽致,我们只需要大力牛顿迭代即可就是这么大坨东西求起导来比较恶心
最后得到这个式子:

$$ F \equiv F_0-\frac{x\exp \left(\frac{2F_0-F_0^2}{2-2F_0} \right)-F}{x\exp \left(\frac{2F_0-F_0^2}{2-2F_0} \right)\left(\frac{1}{2}+\frac{1}{2(F_0-1)^2} \right)-1} $$

最后 $F$ 的第 $i$ 项除以 i,荒漠个数即为 $n![x^n]\exp F$ .


P5401 [CTS2019] 珍珠

经典题目。

不难发现能够得到 $m$ 个合法瓶子当且仅当出现奇数次的变量个数 $\le n - 2m$,那么我们可以暴力写出 $\operatorname{EGF}$ :

$$ ans=\sum_{i=0}^{n-2m}\binom{D}{i} [x^n]\left( \frac{e^x+e^{-x}}{2} \right)^{D-i}\left( \frac{e^x-e^{-x}}{2} \right)^i $$

然而这样会发现 $[x^n]\left( e^x+e^{-x} \right)^{D-i}\left( e^x-e^{-x} \right)^i$ 根本没法算,展开都难。
这种情况下可以考虑二项式反演,它可以把这个东西变成 $x^n^{D-i}\left( e^x-e^{-x} \right)^i$ 这样就只需要展开一个二项式了。
套路地设 $f_k$ 为至少有 $k$ 个出现奇数次的变量的方案数,$g(k)$ 则是恰好有 $k$ 个。
那么有:

$$ \begin{aligned} g(k)&=\sum_{i \ge k} \binom{i}{k}(-1)^{i-k}f(i)\ f(k)&=\binom{D}{k}nx^n^{D-k}\left( \frac{e^x-e^{-x}}{2} \right)^k\ &=\binom{D}{k}2^{-k}nx^n^{D-k}\sum_{i=0}^{k}\binom{k}{i}(-1)^i(e^x)^{k-i}e^{-ix}\ &=\binom{D}{k}2^{-k}\sum_{i=0}^k\binom{k}{i}(-1)^in![x^n]e^{\left(D-2i \right)x}\ &=\binom{D}{k}2^{-k}\sum_{i=0}^k\binom{k}{i}(-1)^i(D-2i)^n \end{aligned} $$

利用二项卷积即可在 $O(D\log D)$ 时间内解决。


P4389 付公主的背包

我们知道,多项式 $\exp$ 的组合意义可以理解为将 $n$ 个带标号小球放入若干个无标号盒子(每个盒子非空)中的方案数,这点在上一题荒漠计数中就有体现,而当去掉小球编号时,我们就要引入一个叫做 $\operatorname{Euler}$ 变换的东西,记作 $\mathcal{E}(F(x))$,它是这样定义的:

$$\mathcal{E}(F(x))=\prod_{i=1}^{+\infty}\frac{1}{(1-x^i)^{[x^i]F(x)}}$$

其中 $[x^i]F(x)$ 表示大小为 $n$ 的集合有多少种,实际上这就是完全背包的生成函数,$i$ 代表物品体积,$[x^i]F(x)$ 代表体积为 $i$ 的物品有多少种,最后卷起来就行,$[x^i]\mathcal{E}(F(x))$ 就代表了物品总体积为 $i$ 的选择方案数。

然而我们发现这个定义式无法在较低复杂度内算出来,所以我们要把它化成更实用的形式,记 $G(x)=\mathcal{E}(F(x))$:

$$ \begin{aligned} \ln G(x) &= -\sum_{i=1}^{+\infty} [x^i]F(x)\ln(1-x^i)\ (\ln G(x))' &= \sum_{i=1}^{+\infty} \frac{[x^i]F(x) \cdot ix^{i-1}}{1-x^i} \ &=\sum_{i=1}^{+\infty} [x^i]F(x) \cdot ix^{i-1}\sum_{j=0}^{+\infty} x^{ij}\ &=\sum_{i=1}^{+\infty} \sum_{j=0}^{+\infty}[x^i]F(x) \cdot ix^{i (j+1)-1}\ \ln G(x) &= \sum_{i=1}^{+\infty} \sum_{j=0}^{+\infty}[x^i]F(x) \frac{x^{i (j+1)}}{j+1} \ &= \sum_{j=1}^{+\infty}\frac{1}{j} \sum_{i=0}^{+\infty}[x^i]F(x)(x^j)^i\ &= \sum_{i=1}^{+\infty}\frac{F(x^j)}{j}\ G(x) &= \exp \left(\sum_{i=1}^{+\infty}\frac{F(x^i)}{i}\right) \end{aligned} $$

这样我们想得到模 $x^m$ 意义下的 $\mathcal{E}(F(x))$ 只需要 $O(m \ln m)$ 计算 $\sum_{i=1}^{+\infty}\frac{F(x^i)}{i}$,$O(m \log m)$ 计算 $\exp$ 即可。


2022 山东一轮省队集训【整数拆分(partition)】

根据题意,不难写出答案:

$$[x^n]\frac{1}{(1-x)^{k+1}\prod_{i \ge 1}(1-x^{m^i})^k}$$

$n,m$ 都在 $10^{18}$ 量级,观察到这就是线性递推的形式,直接套用线性递推的处理方式:

$$ \begin{aligned} &[x^n]\frac{P(x)}{(1-x)^{c}\prod_{i \ge 1}(1-x^{m^i})^k}\ =&[x^n]\frac{P(x)\left(\frac{1-x^m}{1-x}\right)^c}{(1-x^m)^{c+k}\prod_{i\ge 2}(1-x^{m^i})^k}\ =&[x^{\lfloor \frac{n}{m} \rfloor}]\frac{\left[P(x)\left(\frac{1-x^m}{1-x}\right)^{k+1} \right]{n \bmod m}}{(1-x)^{c+k}\prod{i \ge 1}(1-x^{m^i})^k} \end{aligned} $$

$[F]_r$ 表示提取 $F$ 的 $kr(k\in \mathbb{N})$ 项得到的多项式自个发明的,不知道有没有正式记法,注意这里计算分子时先计算 $P(x)(1-x)^{-c}$ 即可做到每层 $O(c^2)$ 时间花费,当 $n=0$ 时直接返回 $[x^0]P(x)$ 即可,总复杂度 $O(k^2\log^3n)$。


AGC060D Same Descent Set

这题主要是容斥,GF部分是次要的。

设 $f(S)$ 表示下降集为 $S$ 的集合个数,$g(S)$ 表示下降集是 $S$ 的子集的排列个数。

根据子集反演,有:

$$ \begin{aligned} g(S) &= \sum_{T \subset S} f(T)\ f(S) &= \sum_{T \subset S} (-1)^{|T|-|S|} g(T) \end{aligned} $$

接下来是推式子时间:

$$ \begin{aligned} \sum_{S} f^2(S) &= \sum_{S} (\sum_{T \subset S} (-1)^{|T| - |S|} g(T))^2\ &= \sum_{S} \sum_{T_1 \subset S} (-1)^{|T_1| - |S|} g(T_1) \sum_{T_2 \subset S} (-1)^{|T_2| - |S|} g(T_2)\ &= \sum_{S} \sum_{T_1, T_2 \subset S} (-1)^{|T_1| + |T_2|} g(T_1)g(T_2)\ &= \sum_{T_1, T_2} (-1)^{|T_1|+|T_2|} g(T_1)g(T_2) \sum_{S \supseteq T_1 \cup T_2 } 1\ &= \sum_{T_1, T_2} (-1)^{|T_1|+|T_2|} g(T_1)g(T_2) 2^{n-1-|T_1 \cup T_2|}\ &= \sum_{T_1, T_2} (-1)^{|T_1|+|T_2|} g(T_1)g(T_2) 2^{n-1-|T_1|-|T_2| + |T_1 \cap T_2|}\ &= 2^{n+1} \sum_{T_1, T_2} (-1)^{|T_1|+|T_2|} g(T_1)g(T_2) 2^{-|T_1|-1-|T_2|-1} \sum_{S \subset |T_1 \cap T_2|} 1\ &= 2^{n+1} \sum_{S} \left (\sum_{T \supseteq S} \left ( -\dfrac{1}{2} \right )^{|T|+1}g(T) \right )^2\ &= 2^{n+1}(n!)^2 \sum_{ s_1+s_2+\dots +s_{m}=n, s_i>0 } \prod_{i=1}^m h^2(s_i)\

h(n) &= \sum_{ s_1+s_2+\dots +s_{m}=n, s_i>0 } \left( -\dfrac{1}{2} \right)^{m} \dfrac{1}{\prod_{i=1}^m s_i!}\ A(x) &= \sum_{i \ge 1} -\dfrac{1}{2i!}x^i\ H'(x) &= \sum_{i \ge 1} h(i)x^i\ &= \sum_{i \ge 1} A^i(x)\ &= \dfrac{A(x)}{1-A(x)}\ H(x) &= \sum_{i \ge 0} h^2(i)x^i\ ans &= 2^{n+1}(n!)^2 [x^n] \sum_{i \ge 1} H^i(x) = 2^{n+1}(n!)^2 [x^n] \dfrac{H(x)}{1-H(x)} \end{aligned} $$

过程中将集合并的大小转化为交的大小是关键一步。

使用多项式求逆即可在 $O(n \log n)$ 时间内解决。


bzoj3305

不难设计 dp:$f_{i,j}$ 表示考虑了前 $i$ 个位置,左括号减右括号数量为 $j$,有转移:

$$ f_{i,j} = f_{i,j-1} + (j+1)f_{i,j+1} $$

设 $F_{i}(x) = \sum_{j \ge 0} f_{i,j}x^j$,可以得到:

$$ F_{i} = xF_{i-1}+F'_{i-1} $$

两侧同时乘上 $e^{\frac{x^2}{2}}$,设 $G_i = e^{\frac{x^2}{2}} F_{i}$,恰好能配出来:

$$ G_i = G'_{i-1} $$

$G_0 = e^{\frac{x^2}{2}}$,$2n$ 阶导的常数项即为 $\dfrac{(2n)^{\underline{n}}}{2^{n}}$。

upd:GF太不优美了,JOISC 2023 D1T2 有完美的组合解释。

[多项式] 多项式模板大杂烩

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2022-06-08 20:10:58

多项式模板大杂烩

本文中 $f(x),F,F(x)$ 均指同一多项式。 $f(x)= \{a_0,a_1,...,a_n\}$ 表示以系数表示法表示的 $n$ 次多项式 $f(x)$,$[x^k]F=a_k$。


$1)$ FFT(NTT)

前置:单位根原根

1. FFT:

FFT依靠分治实现。

具体来说,每次我们将 $f(x)$ 的系数按照奇偶次项分开,假设我们有一个最高次为 $n-1(n=2^k)$ 次的多项式:

$$f(x)=\{a_0,a_1,...,a_{n-1}\}$$

我们的目标是 $\forall i \in [0,n-1]$,求出 $f(\omega_{n}^{i})$

$$ \begin{aligned} f_{0}(x)&=\{a_0,a_2,...,a_{n-2}\} \ f_{1}(x)&=\{a_1,a_3,...,a_{n-1}\} \end{aligned} $$

那么我们有

$$f(x)=f_{0}(x^2)+x \cdot f_{1}(x^2)$$

根据单位根的性质有 $k \in [0,\frac{n}{2})$ 时:

$$ \begin{aligned} f(\omega_{n}^{k})&=f_0((\omega_{n}^{k})^2)+\omega_{n}^{k} \cdot f_1((\omega_{n}^{k})^2) \ &= f_0(\omega_{\frac{n}{2}}^{k})+\omega_{n}^{k} \cdot f_1(\omega_{\frac{n}{2}}^{k})\ f(\omega_{n}^{k+\frac{n}{2}})&=f_0((\omega_{n}^{k+\frac{n}{2}})^2)+\omega_{n}^{k+\frac{n}{2}} \cdot f_1((\omega_{n}^{k+\frac{n}{2}})^2) \ &= f_0(\omega_{\frac{n}{2}}^{k})-\omega_{n}^{k} \cdot f_1(\omega_{\frac{n}{2}}^{k}) \end{aligned} $$

特别的,当 $n=1$ 时, $f(\omega_{1}^{0})=\{(\omega_{1}^{0},a_0)\}$ .

也就是说,如果已知 $f_0(x),f_1(x)$ 的点值表示法,那么我们就可以枚举 $k$,在 $O(n)$ 时间内得到 $f(x)$ 的点值表示法,又因为每次 $f_0,f_1$ 长度减半,最多进行 $O(\log n)$ 次,所以总复杂度为 $O(n\log n)$ .

2. 蝴蝶变换:

在FFT的实际实现中,我们不采用递归,而是使用了叫做“蝴蝶变换”的东西。
观察分治过程 $(n=8)$:

第一层: $\{a_{(000)2},a{(001)2},a{(010)2},a{(011)2},a{(100)2},a{(101)2},a{(110)2},a{(111)2}\}$
第二层: $\{a
{(000)2},a{(010)2},a{(100)2},a{(110)2}\},\{a{(001)2},a{(011)2},a{(101)2},a{(111)2}\}$
第三层: $\{a
{(000)2},a{(100)2}\},\{a{(010)2},a{(110)2}\},\{a{(001)2},a{(101)2}\},\{a{(011)2},a{(111)2}\}$
第四层: $\{a
{(000)2}\},\{a{(100)2}\},\{a{(010)2}\},\{a{(110)2}\},\{a{(001)2}\},\{a{(101)2}\},\{a{(011)2}\},\{a{(111)_2}\}$

我们发现个位置叶子层上第 $i$ 个位置对应的 $a$ 的下标恰好是 $i$ 二进制反转后的结果(其实这是必然),所以我们可以直接把 $\{a_{i}\}$ 变换为最后一层,然后一层一层地向上合并。

另外,FFT要求多项式必须是 $2^k-1$ 次(即长度是 $2^k$),在系数表示法后补 $0$ 即可。

3. IFFT:

IFFT即FFT的逆变换,将点值表示法转换回系数表示法。
现在我们已知 $f(x)=\{(\omega_{n}^{0},f(\omega_{n}^{0})),...,(\omega_{n}^{n-1},f(\omega_{n}^{n-1}))\}$,假设它对应的系数表示法是 $\{a_i\}$,那么有:

$$a_k=\frac{1}{n} \sum_{i=0}^{n-1}f(\omega_{n}^i)(\omega_{n}^{-k})^i$$

可以发现其实是构造了一个多项式 $B(x)=\{f(\omega_{n}^{0}),...,f(\omega_{n}^{n-1})\}$,使得 $a_k=\frac{1}{n}B(\omega_{n}^{-k})$,将刚才FFT式子中的 $\omega_{n}^k$ 变为 $\omega_{n}^{-k}$,再做一遍FFT,乘以 $\frac{1}{n}$ 即可。

证一下:

$$ \begin{aligned} \sum_{i=0}^{n-1}f(\omega_{n}^i)(\omega_{n}^{-k})^i &= \sum_{i=0}^{n-1}(\omega_{n}^{-k})^i\sum_{j=0}^{n-1}a_j(\omega_{n}^{i})^j \ &= \sum_{j=0}^{n-1}a_j\sum_{i=0}^{n-1}(\omega_{n}^{j-k})^i \ &= na_k+ \sum_{j=0 & j\not=k}^{n-1}a_j\frac{\omega_{n}^{n(j-k)}-1}{\omega_{n}^{j-k}-1}\ &=na_k \ \ (\omega_{n}^{n(j-k)}-1=\omega_{1}^{j-k}-1=1-1=0)\ \end{aligned} $$

FFT代码就不放了,待会直接看NTT的就行。

4. NTT&INTT:

原根 $(g_n^k)$,就是模 $p$ 意义下的单位根,具有与单位根相似的性质,得以用来计算模 $p$ 意义下的多项式乘法,直接套用FFT的式子即可。同时因为要求 $n=2^k$ 且 $n \mid p-1$,所以 $p$ 都需要可以表示为 $a\cdot2^k+1$ (当然通过中国剩余定理可以做到任意模数多项式乘法(MTT),这个后面再说)。

最常见的NTT模数是 $998244353$,原根为 $3$,题中见到不常见的模数时先检查是不是多项式模数。
模数的原根(如果存在,其中素数一定有原根)也不会太大,遇到没见过的直接从 $3$ 开始暴力枚举然后检查即可。

代码:

#include <bits\/stdc++.h>
using namespace std;

const int Mod = 998244353;\/\/Mod = 2 ^ 23 * 119 + 1
const int g = 3, ginv = 332748118;\/\/998244353的一个原根及其逆元
const int MAXN = 2.1e6;

int n, m;
int a[MAXN], b[MAXN];
int rev[MAXN], bit, tot;

inline int qpow(int a, int b) {
    int base = a, ans = 1;
    while (b) {
        if (b & 1) ans = 1ll * ans * base % Mod;
        base = 1ll * base * base % Mod;
        b >>= 1;
    }
    return ans;
}

void NTT(int a[], bool inv) {
    for (int i = 0; i < tot; i++)
        if (i < rev[i])
            swap(a[i], a[rev[i]]);

    for (int len = 1; len < tot; len <<= 1) {
        int gn = qpow(inv ? ginv : g, (Mod - 1) \/ (len << 1));
        for (int i = 0; i < tot; i += len * 2) {
            int g0 = 1;
            for (int j = 0; j < len; j++) {
                int x = a[i + j], y = 1ll * g0 * a[i + j + len] % Mod;
                a[i + j] = (x + y) % Mod;
                a[i + j + len] = (x - y + Mod) % Mod;
                g0 = 1ll * g0 * gn % Mod;
            }
        }
    }
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i <= n; i++) scanf("%d", &a[i]);
    for (int i = 0; i <= m; i++) scanf("%d", &b[i]);
    while ((1 << bit) <= n + m) bit++;
    tot = 1 << bit;
    for (int i = 1; i < tot; i++) rev[i] = rev[i >> 1] >> 1 | (i & 1) << bit - 1;
    NTT(a, 0), NTT(b, 0);
    for (int i = 0; i < tot; i++) a[i] = 1ll * a[i] * b[i] % Mod;
    NTT(a, 1);
    int inv = qpow(tot, Mod - 2);
    for (int i = 0; i <= n + m; i++) printf("%d ", 1ll * a[i] * inv % Mod);\/\/ \/ tot 变为 * inv
    return 0;
}

有了乘法,往后就是多项式的各种操作了,尽量只写式子。
以下运算均在模 $998244353$ 意义下进行,如无特殊说明,都有 $n \le 10^5$ .


$2)$ 多项式求逆

给定 $n-1$ 次多项式 $f(x)$,求多项式 $f^{-1}(x)$,使得 $f(x)*f^{-1}(x) \equiv 1 \pmod {x^n}$ .

显然在 $n=1$ 时,$f^{-1}(x) \equiv a_0^{-1} \pmod x$ .

假设我们现在已经求出了 $G$,满足 $FG \equiv 1 \pmod {x^{\frac{n}{2}}}$,目标是求出 $H$,满足 $FH \equiv 1 \pmod {x^n}$ .

$$ \begin{aligned} F(G-H) &\equiv 0 \pmod {x^{\frac{n}{2}}}\ G-H &\equiv 0 \pmod {x^{\frac{n}{2}}}\ (G-H)^2 &\equiv 0 \pmod {x^{n}} \ H^2 &\equiv 2GH - G^2 \pmod {x^{n}}\ H &\equiv 2G-FG^2 \pmod {x^{n}}\ \end{aligned} $$

$n=1$ 时的答案已经知道,从模 $x$ 开始倍增递推即可。
在 $O(n \log n)$ 时间内解决。

inline void calc_inv(int a[], int n) {\/\/a ← a^(-1) (mod x^n) 多项式求逆
        int bas = 1, len = 4, bit = 2;
        bool opt = 0;
        memset(inv[0], 0, sizeof(inv[0]));
        inv[0][0] = qpow(a[0], Mod - 2);
        while (bas < n) {
            calc_rev(bit, len);
            opt ^= 1;
            memset(inv[opt], 0, sizeof(inv[opt]));
            for (int i = 0; i < bas; i++) inv[opt][i] = 2ll * inv[opt ^ 1][i] % Mod;\/\/bas后面都是0
            mul(inv[opt ^ 1], inv[opt ^ 1], len);
            mul(inv[opt ^ 1], a, len);
            bas <<= 1, len <<= 1, bit++;
            for (int i = 0; i < bas; i++) inv[opt][i] = (inv[opt][i] - inv[opt ^ 1][i] + Mod) % Mod;
        }
        for (int i = 0; i < n; i++) a[i] = inv[opt][i];
    }

$3)$ 多项式开根

给定 $n-1$ 次多项式 $f(x)$,保证常数项为 $1$,求次数 $< n$ 的多项式 $g(x)$,使得 $g^2(x) \equiv f(x) \pmod {x^n}$,多解情况下取常数项较小的作为答案。

显然 $n=1$ 时,$\sqrt F \equiv 1 \pmod x$ .
假设已经求出 $G$,满足 $G^2 \equiv F \pmod {x^\frac{n}{2}}$,目标求出 $H$,使得 $H^2 \equiv F \pmod {x^n}$ .

$$ \begin{aligned} G^2-H^2 &\equiv 0 \pmod {x^\frac{n}{2}}\ (G-H)(G+H) &\equiv 0 \pmod {x^\frac{n}{2}}\ \end{aligned} $$

因为要求常数项最小,即 $1$,所以 $H$ 必然与 $G$ 同余。

$$ \begin{aligned} H-G &\equiv 0 \pmod {x^\frac{n}{2}}\ H^2-2GH+G^2 &\equiv 0 \pmod {x^{n}}\ H &\equiv (2G)^{-1}(F+G^2) \pmod {x^{n}}\ H &\equiv 2^{-1}(G^{-1}F+G) \pmod {x^{n}} \end{aligned} $$

结合多项式求逆在 $O(n \log n)$ 时间内解决。

inline void calc_sqrt(int a[], int n) {\/\/a ← sqrt(a) (mod x^n)
        int bas = 1, bit = 2, len = 4;
        bool opt = 0;
        memset(sqr[0], 0, sizeof(sqr[0]));
        sqr[0][0] = 1;
        while (bas < n) {
            opt ^= 1;
            memset(sqr[opt], 0, sizeof(sqr[opt]));
            for (int i = 0; i < bas; i++) sqr[opt][i] = sqr[opt ^ 1][i];
            calc_inv(sqr[opt ^ 1], bas << 1);
            calc_rev(bit, len);
            mul(sqr[opt ^ 1], a, len);
            bas <<= 1, len <<= 1, bit++;
            for (int i = 0; i < bas; i++) sqr[opt][i] = 1ll * inv2 * (sqr[opt ^ 1][i] + sqr[opt][i]) % Mod;
        }
        for (int i = 0; i < n; i++) a[i] = sqr[opt][i];
    }

$4)$ 多项式取模

先介绍一个概念,假设我们有多项式 $f(x)=\{a_0,a_1,...,a_{n-1},a_n\}$,那么我们将 $\{a_n,a_{n-1},...,a_1,a_0\}$ 称为 $f(x)$ 的反射多项式,记作 $F^R$。
显然我们有:

$$f(x)=x^nf(\frac{1}{x})$$

进入正题。

给定 $n-1$ 次多项式 $f(x)$,与 $m-1$ 次多项式 $g(x)$,求多项式 $h(x),r(x)$,满足:

  • $h(x)$ 次数为 $n-m$,$r(x)$ 次数 $<m-1$ .
  • $F=GH+R$ .

不妨设 $R$ 的次数为 $m-2$, 那么有

$$ \begin{aligned} F&=GH+R\ x^{n-1}F&=x^{m-1}G * x^{n-m}H+x^{n-m+1}x^{m-2}R\ F^R&=G^RH^R+x^{n-m+1}R^R\ F^R &\equiv G^RH^R \pmod {x^{n-m+1}}\ H^R &\equiv F^R(G^R)^{-1} \pmod {x^{n-m+1}}\ \end{aligned} $$

结合多项式求逆在 $O(n \log n)$ 时间内解决,最后 $R=F-GH$ 求出 $R$ 即可。

inline void division(int a[], int n, int b[], int m) {
    for (int i = 0; i < n; i++) ta[i] = a[n - i - 1];
    for (int i = 0; i < m; i++) tb[i] = b[m - i - 1];
    calc_inv(tb, n - m + 1);

    int len = 1, bit = 0;
    while (len < n - m + 1 << 1) len <<= 1, bit++;
    calc_rev(bit, len);
    mul(ta, tb, len);

    reverse(ta, ta + n - m + 1);
    for (int i = 0; i < n - m + 1; i++) printf("%d ", ta[i]); puts("");
    
    len = 1, bit = 0;
    while (len < m << 1) len <<= 1, bit++;
    for (int i = n - m + 1; i < len; i++) ta[i] = 0;
    calc_rev(bit, len);
    mul(b, ta, len);

    for (int i = 0; i < m - 1; i++) printf("%d ", (a[i] - b[i] + Mod) % Mod);
}

$5)$ 多项式 $\ln$

首先我们要知道几个式子:

  • 若 $f(x)= \ln x$,则 $f'(x)= \dfrac{1}{x}$ . (自然对数函数求导)
  • 若 $f(x)= k \cdot x^a$,则 $f'(x)= ka \cdot x^{a-1}$ . (幂函数求导)
  • 若 $f(x)= k \cdot x^a$,则 $\int f(x)dx= \frac{k}{a+1}x^{a+1}+C$ . (幂函数求积)
  • 若 $f(x)= g(h(x))$,则 $f'(x)=g'(h(x))h'(x)$ . (链式法则)

另外,对多项式求导(求积)等于对各个单项式求导(求积)后再加起来。

进入正题:

给定 $n-1$ 次多项式 $f(x)$,求多项式 $g(x)$,满足 $g(x) \equiv \ln f(x) \pmod {x^n}$,保证 $[x^0]F=1$ .
$\ln (1-f(x)) = - \sum_{i=0}^{+ \infty} \frac{f^i(x)}{i}$

根据上面的式子,我们有:

$$ \begin{aligned} G &\equiv \ln F \pmod {x^n}\ G' &\equiv F'F^{-1} \pmod {x^n} \ G &\equiv \int F'F^{-1} \pmod {x^n} \end{aligned} $$

结合多项式求逆在 $O(n \log n)$ 时间内解决。

void ln(int a[], int n) {
    memset(ta, 0, sizeof(ta));
    for (int i = 1; i < n; i++) ta[i - 1] = 1ll * a[i] * i % Mod;
    calc_inv(a, n);
    
    int len = 1, bit = 0;
    while (len < n << 1) len <<= 1, bit++;
    calc_rev(bit, len);
    mul(a, ta, len);

    for (int i = n - 1; i; i--) a[i] = 1ll * a[i - 1] * qpow(i, Mod - 2) % Mod;
}

$5 \frac{1}{2} )$ $\ \text Newton's \ \text Method$

在 $\exp$ 之前,先介绍一个比较好用的东西——多项式牛顿迭代法。

给定以多项式为参数的函数 $f(x)$,求模 $x^n$ 意义下的多项式 $g(x)$,使得 $f(g(x)) \equiv 0 \pmod {x^n}$ .

依然是倍增的思想,假设我们已经求出来了模 $x^{\frac{n}{2}}$ 意义下的根 $g_0(x)$,那么在模 $x^n$ 意义下的根 $f(x)$ 满足:

$$f(x) \equiv f_0(x)-\frac{g(f_0(x))}{g'(f_0(x))} \pmod {x^n}$$

OI wiki 上给出了证明,需要知道 泰勒展开
另外,在求导的时候,我们的数域拓展到了多项式,例如当 $f(F)=GF^2+A$ 时,有 $f'(F)=2GF$ .

有了牛顿迭代,上面提到的多项式求逆和多项式开根就可以用此方法求出,举一个开根的例子:

因为有 $g^2(x) \equiv f(x) \pmod {x^n}$,所以我们就是要求出 $h(g(x))=g^2(x)-f(x) \equiv 0 \pmod {x^n}$ 的根,那么有:

$$ \begin{aligned} G &\equiv G_0-\frac{H(G_0)}{H'(G_0)}\ &\equiv G_0-\frac{G_0^2-F}{2G_0}\ &\equiv \frac{G_0^2+F}{2G_0}\ &\equiv 2^{-1}(G_0+FG_0^{-1}) \pmod {x^n} \end{aligned} $$

与另一种方法推出来的式子一致。


$6)$ 多项式 $\exp$

给定 $n-1$ 次多项式 $f(x)$,求多项式 $g(x)$,满足 $g(x) \equiv e^{f(x)}$,保证 $[x^0]F=0$ .
其中 $\exp f(x) = \sum_{i=0}^{+ \infty} \frac{f^i(x)}{i!}$ .

就是求 $h(g(x))= \ln g(x)-f(x) \equiv 0 \pmod {x^n}$ 的根,根据牛顿迭代有:

$$ \begin{aligned} G &\equiv G_0- \frac{ \ln G_0 - F}{ \frac{1}{G_0}}\ &\equiv G_0(1+F- \ln G_0) \pmod {x^n} \end{aligned} $$

结合多项式 $\ln$ 在 $O(n \log n)$ 时间内解决。

void calc_exp(int a[], int n) {
    int bas = 1, bit = 2, len = 4;
    bool opt = 0;
    memset(exp[0], 0, sizeof(exp[0]));
    exp[0][0] = 1;
    while (bas < n) {
        opt ^= 1;
        memset(exp[opt], 0, sizeof(exp[opt]));
        for (int i = 0; i < bas; i++) exp[opt][i] = exp[opt ^ 1][i];
        calc_ln(exp[opt ^ 1], bas << 1);
        for (int i = 0; i < bas << 1; i++) exp[opt ^ 1][i] = ((i == 0) + a[i] - exp[opt ^ 1][i] + Mod) % Mod;
        calc_rev(bit, len);
        mul(exp[opt], exp[opt ^ 1], len);
        for (int i = bas << 1; i < len; i++) exp[opt][i] = 0;
        bas <<= 1, bit++, len <<= 1;
    }
    for (int i = 0; i < n; i++) a[i] = exp[opt][i];
}

$7)$ 多项式快速幂

给定 $n-1$ 次多项式 $f(x)$,求多项式 $g(x)$,满足 $g(x) \equiv f^k(x) \pmod {x^n} \ (k \le 10^{10^5})$ 保证 $[x^0]F=1$.

可以看到,$k$ 足足有 $1e5$ 位,$O(n \log n \log k)$ 不用想了。
……但是我们有 $\ln$ 和 $\exp$ 啊,所以有:

$$F^k \equiv e^{k \ln F} \pmod {x^n}$$

所以这玩意当 $i \equiv j \pmod {998244353}$ 时,$F^i \equiv F^j \pmod {x^n}$ .
然后……就没了。

void calc_qpow(int a[], int n, int k) {
    calc_ln(a, n);
    for (int i = 0; i < n; i++) a[i] = 1ll * a[i] * k % Mod;
    calc_exp(a, n);
}

$8)$ 多项式多点求值

给定 $n-1$ 次多项式 $f(x)$ 以及 $m$ 个非负整数 $\{a_i\}$,$\forall i \in [1,m]$,求出 $f(a_i)$ .

默认小写字母表示列向量,大写字母表示矩阵。

矩阵转置:对于矩阵 $A$ 中任意一个元素 $a_{i,j}$,将 $i,j$ 交换,得到的矩阵称为 $A$ 的转置矩阵,记作 $A^T$,其满足以下性质:

  1. $(A^T)^T=A$
  2. $(A+B)^T=A^T+B^T$
  3. $(AB)^T=B^TA^T$

转置原理:又称特勒根原理,其指出当我们要求解 $b=Aa$ 时,可以找到 $b'=A^Ta$ 的一种高效求解方法,将方法中每一步对 $a$ 的操作拆解出来,即把 $A^T$ 分解为 $E_1E_2...E_n$,然后倒序执行每一个操作的转置,即将 $E_n^TE_{n-1}^T...E_1^T$ 施加于 $a$,这样我们就得到了 $b$ .

关于操作的转置,比如计算卷积 $\{c_0,c_1,...,c_{n+m}\}=\{b_0,b_1,...,b_m\}*\{a_0,a_1,...a_n\}$ 时,可以看作:

$$ \begin{bmatrix} c_0\ c_1\ c_2\ \ dots\ c_{n+m} \end{bmatrix}

\begin{bmatrix} b_0 &0 &0 &\cdots &0\ b_1 &b_0 &0 &\cdots &0\ b_2 &b_1 &b_0 &\cdots &0\ \ dots &\ dots &\ dots &\ddots &\ dots\ b_{n+m} &b_{n+m-1} &b_{n+m-2} &\cdots &b_0 \end{bmatrix} * \begin{bmatrix} a_0\ a_1\ a_2\ \ dots\ a_{n+m} \end{bmatrix} $$

将方阵转置,我们得到:

$$ \begin{bmatrix} c'0\ c'_1\ c'_2\ \ dots\ c'{n+m} \end{bmatrix}

\begin{bmatrix} b_0 &b_1 &b_2 &\cdots &b_{n+m}\ 0 &b_0 &b_1 &\cdots &b_{n+m-1}\ 0 &0 &b_0 &\cdots &b_{n+m-2}\ \ dots &\ dots &\ dots &\ddots &\ dots\ 0 &0 &0 &\cdots &b_0 \end{bmatrix} * \begin{bmatrix} a_0\ a_1\ a_2\ \ dots\ a_{n+m} \end{bmatrix} $$

此时 $c'i=\sum{j=i}^{n}a_{j}b_{j-i}$,即卷积的转置操作,记作 $\{b_i\}*^T\{a_i\}$。

回到正题,假设答案序列为 $\{b_1,b_2,...,b_n \}$,我们有 $(n=m)$:

$$ \begin{bmatrix} b_1\ b_2\ \ dots\ b_n \end{bmatrix}

\begin{bmatrix} 1 &a_1 &a_1^2 &\cdots &a_1^{n-1}\ 1 &a_2 &a_2^2 &\cdots &a_2^{n-1}\ \ dots &\ dots &\ dots &\ddots &\ dots\ 1 &a_{n} &a_{n}^2 &\cdots &a_{n}^{n-1} \end{bmatrix} * \begin{bmatrix} f_0\ f_1\ \ dots\ f_{n-1} \end{bmatrix} $$

转置之后得到:

$$ \begin{bmatrix} b'_1\ b'_2\ \ dots\ b'_n \end{bmatrix}

\begin{bmatrix} 1 &1 &1 &\cdots &1\ a_1 &a_2 &a_3 &\cdots &a_n\ \ dots &\ dots &\ dots &\ddots &\ dots\ a_1^{n-1} &a_2^{n-1} &a_3^{n-1} &\cdots &a_n^{n-1} \end{bmatrix} * \begin{bmatrix} f_0\ f_1\ \ dots\ f_{n-1} \end{bmatrix} $$

设 $\{b'_i\}$ 的 $\operatorname{OGF}$ 为 $B$,有:

$$ \begin{aligned} B &= \sum_{i=1}^{+\infty} b_ix^i\ &= \sum_{i=1}^{+\infty} \left(\sum_{j=1}^{n} a_j^{i-1}f_{j-1} \right)x^i\ &= \sum_{j=1}^{n} f_{j-1} \left(x\sum_{i=0}^{+\infty}a_j^ix^i \right)\ &= x\sum_{i=1}^{n}\frac{f_{i-1}}{1-a_ix} \end{aligned} $$

这个式子可以用分治 $\operatorname{FFT}$ 解决,维护 $M_{l,r}, P_{l,r}$ 分别表示区间 $[l,r]$ 的分子和分母,有:

$$ \begin{aligned} M_{l,l}&=1-a_lx\ P_{l,l}&=f_{l-1}\ M_{l,r}&=M_{l,mid}M_{mkd+1,r}\ P_{l,r}&=P_{l,mid}M_{mid+1,r}+P_{mid+1,r}*M_{l,mid} \end{aligned} $$

最后答案是 $M_{1,m}^{-1}P_{1,m}$ .
整个分治过程都可以看作是对 $\{f_i\}$ 进行的一系列初等变换,因此直接把所有操作倒序并执行其转置操作。
具体来讲,先计算出常数 $M_{l,r}$ ,然后令 $P_{1,m} = \{f_i\}
^TM_{1,m}^{-1}$,接着从根向叶节点分治,将上述分治过程中的转移全部操作全部换为其转置操作,即:

$$ \begin{aligned} P_{l,mid}&=P_{l,r}^TM_{mid+1,r}\ P_{mid+1,r}&=P_{l,r}^TM_{l,mid} \end{aligned} $$

按顺序输出 $P_{l,l}$ 的常数项即可。

\/\/这里只放了第二次分治
void solve2(int p, int l, int r) {
    if (l == r) return printf("%d\n", F[p][0]), void();
    int mid = (l + r) >> 1;
    F[p << 1] = mulT(F[p], M[p << 1 | 1]); F[p << 1].resize(mid - l + 1);
    F[p << 1 | 1] = mulT(F[p], M[p << 1]); F[p << 1 | 1].resize(r - mid);
    solve2(p << 1, l, mid);
    solve2(p << 1 | 1, mid + 1, r);
}

$9)$ 线性递推

已知数列 $\{f_i\}$ 满足 $k$ 阶线性递推关系:

$$f_i=\sum_{j=1}^kg_jf_{i-j}$$

给定 $g_{1...k},f_{0...k-1}$,求 $f_n$ $(n \le 10^9,k \le 32000)$ .

(当 $k$ 较小时,一般直接使用矩阵快速幂解决)

考虑 $\operatorname{OGF}$,我们有:

$$ \begin{aligned} F(x)&=G(x)F(x)+R(x)\\ F(x)&=\frac{R(x)}{1-G(x)} \end{aligned} $$

其中 $\operatorname{deg}R < k$,用来调整 $0$ 到 $k-1$ 位。
那么只要题目满足 $F(x)=\dfrac{R(x)}{Q(x)}$ 我们就可以称其具有线性递推关系,下面介绍一下这种关系的机械处理方法。

$$ \begin{aligned} [x^n]F(x)&=[x^n]\frac{R(x)}{Q(x)}\ &= [x^n]\frac{R(x)Q(-x)}{Q(x)Q(-x)}\ \end{aligned} $$ 令 $U(x^2)=Q(x)Q(-x)$,$A(x^2)+xB(x^2)=R(x)Q(-x)$,有: $$ \begin{aligned} [x^n]F(x) &= [x^n]\frac{A(x^2)}{U(x^2)}+[x^n]x\frac{B(x^2)}{U(x^2)}\ &= \begin{cases} [x^{\frac{n}{2}}]\dfrac{A(x)}{U(x)}& n \bmod 2=0\ [x^{\frac{(n-1)}{2}}] \dfrac{B(x)}{U(x)}& n\bmod 2=1 \end{cases} \end{aligned} $$

$n$ 每次会减小一半,时间复杂度 $O(k\log k\log n)$,还可以进一步将常数 $\times \dfrac{1}{3}$,具体参考杜爷的博客 .

当然对于分母的处理方式不一定是乘上 $Q(-x)$,只要能将分母化为 $U(x^b)$ 的形式,就能从 $O(n)$ 降为 $O(\log_{b}n)$ .

模板还有很多,有时间再补(

CF1179D Fedor Runs for President 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2022-07-23 10:50:56

洛谷传送门:CF1179D Fedor Runs for President


模拟赛的时候居然自己做出来了,用的换根dp,看到题解区还没有讲到换根的,于是写篇题解介绍一下。


step 1. 问题转化:

考虑加上一条边后,新增了多少条路径。
此时原树变为了一棵基环树,将其看作是把若干子树的根顺次连接成环得到的图,当且仅当两个点分别处于不同子树的时候,其之间的路径会经过环,也就新增了一条路径。若有 $k$ 棵子树,第 $i$ 棵大小为 $siz_i$,那么路径条数为:

$$ \begin{aligned} &\frac{n(n-1)}{2}+\frac{1}{2}\sum_{i=1}^{k}siz_i(n-siz_i)\ =&\frac{n(n-1)}{2}+\frac{1}{2} \left(n^2-\sum_{i=1}^{k}siz_i^2 \right) \end{aligned} $$

式子的意义是原树路径数量+两点在不同子树中的点对数量。

观察式子发现只需要最小化 $\sum_{i=1}^{k}siz_i^2$,即子树大小平方和,此时我们可以将问题看作选择原树中的一条路径,最小化断开路径上的边后形成的连通块的大小的平方和。
对于这种寻找路径的问题,一种思路是像其它题解一样在路径端点的lca处拼接两条来自不同子树的路径;还有一种就是枚举节点,计算以该点为路径其中端点的答案,然后考虑用换根 dp 优化。

step 2. 换根 dp + 斜率优化

考虑怎样得到以某一个节点为路径端点时的答案,将其作为整棵树的根,设 $f_u$ 为以 $u$ 为根的子树中路径从 $u$ 向下延伸时的答案,首先注意到路径一定会向下延伸直到叶子节点($siz$ 总和不变,份数越多平方和越小),易得转移方程:

$$ f_u= \begin{cases} 1 & \text{u is leaf}\ \min_{son} \{f_{son}+(siz_u-siz_{son})^2\} & \text{otherwise} \end{cases} $$

暴力枚举每个节点作为根即可得到 $O(n^2)$ 做法、

接着考虑换根 dp 优化,设当前的根为 $u$,要将根换到 $s$,关键在于如何计算除去子树 $s$ 后的 $f_u$。设 $f_u$ 是从儿子 $p$ 转移而来,观察转移方程:

$$ \begin{aligned} f_u&=f_p+(siz_u-siz_p)^2\ &=f_p+siz_u^2-2siz_usiz_p+siz_p^2 \end{aligned} $$

移项得到经典斜率优化式子:

$$ 2siz_usiz_p+f_u-siz_u^2=f_p+siz_p^2 $$

每个儿子 $son$ 看作点 $(2siz_{son},f_{son}+siz_{son}^2)$,维护下凸壳,查找去掉子树 $s$ 后的 $f_u$ 的最优转移点就是查找斜率为 $siz_u-siz_s$ 与下凸壳的切点,时间复杂度 $O(n \log n)$偷懒使用了不用动脑子的二分

代码,具体实现参考注释:

\/\/CF1179D Fedor Runs for President
#include <bits\/stdc++.h>
using namespace std;

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

typedef long long ll;
const int MAXN = 5e5+5;

ll n, siz[MAXN];
ll f[MAXN], ans;

vector<int> T[MAXN];

struct convex {
	vector<int> stk;
	int top;
	
	inline ll X(int x) { return 2ll * siz[x]; }
	inline ll Y(int x) { return f[x] + 1ll * siz[x] * siz[x]; }
	inline long double slope(int x, int y) { return (long double) (Y(x) - Y(y)) \/ (X(x) - X(y)); }
	
	inline void push(int x) {
		if (top && X(stk[top]) == X(x) && Y(stk[top]) <= Y(x)) return;
		while (top && X(stk[top]) == X(x) && Y(stk[top]) > Y(x)) top--;\/\/注意处理横坐标相同的情况
		while (top > 1 && slope(stk[top], stk[top - 1]) > slope(x, stk[top - 1])) top--;\/\/查找最小值维护下凸壳
		stk[++top] = x;
	}
	
	inline int query(int k) {
		int L = 1, R = top - 1, ans = stk[top];
		while (L <= R) {
			int mid = (L + R) >> 1;
			if (slope(stk[mid], stk[mid + 1]) >= k) ans = stk[mid], R = mid - 1;
			else L = mid + 1;
		}
		return ans;
	}
} C;

void dfs1(int x, int fa) {
	siz[x] = 1;
	if (siz[x] == 1) return f[x] = 1, void();
	for (int son : T[x]) {
		if (son == fa) continue;
		dfs1(son, x);
		siz[x] += siz[son];
	}
	
	sort(T[x].begin(), T[x].end(), [](int x, int y) { return siz[x] < siz[y]; });\/\/按siz排序,保证横坐标单调
	
	for (int son : T[x]) {
		if (son == fa) continue;
		f[x] = min(f[x], f[son] + (siz[x] - siz[son]) * (siz[x] - siz[son]));
	}
}

ll g[MAXN];

void dfs2(int x, int fa, ll ffa) {
	ll tf = 1e18;
	for (int son : T[x]) {
		if (son == fa) continue;
		tf = min(tf, f[son] + (n - siz[son]) * (n - siz[son]));\/\/计算以x点为根时的答案
		g[son] = ffa + (siz[x] - siz[son]) * (siz[x] - siz[son]);
	}
	ans = min(ans, min(tf, ffa + siz[x] * siz[x]));
	
	if (x != 1 && T[x].size() == 1) return;
	
	C.stk.resize(T[x].size() + 1), C.top = 0;
	int sz = T[x].size();
	for (int k = 0; k < sz; k++) {
		int son = T[x][k];
		if (son == fa) continue;
		int p = C.query(n - siz[son]);
		if (p) g[son] = min(g[son], f[p] + (n - siz[son] - siz[p]) * (n - siz[son] - siz[p]));
		C.push(son);
	}
	
	C.stk.resize(T[x].size() + 1), C.top = 0;
	for (int k = sz - 1; ~k; k--) {
		int son = T[x][k];
		if (son == fa) continue;
		int p = C.query(n - siz[son]);
		if (p) g[son] = min(g[son], f[p] + (n - siz[son] - siz[p]) * (n - siz[son] - siz[p]));
		\/\/对于每个son,计算前缀最优和后缀最优,两者再加上从父节点转移的答案一起取min
		C.push(son);
	}
	
	for (int son : T[x]) {
		if (son == fa) continue;
		dfs2(son, x, g[son]);
	}
}

int main() {
	n = read();
	for (int i = 1; i < n; i++) {
		int u = read(), v = read();
		T[u].push_back(v), T[v].push_back(u);
	}
	
	memset(f, 0x3f, sizeof(f));
	dfs1(1, 0); ans = f[1];
	dfs2(1, 0, 1e18);
	
	printf("%lld", n * (n - 1) \/ 2 + (n * n - ans) \/ 2);
	return 0;
}

CF1716F Bags with Balls 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2022-08-06 12:01:00

洛谷传送门: CF1716F Bags with Balls


暴力推式子大法好


题目实际上就是让我们求:

$$ \sum_{i=0}^{n} i^k \binom{n}{i} \left( \left \lceil \frac{m}{2} \right \rceil \right)^i \left( \left \lfloor \frac{m}{2} \right \rfloor \right)^{n-i} $$

即枚举奇数编号球的个数,计算对应的方案数。

式子中的 $i^k$ 很难搞,考虑将普通幂转为下降幂,因为下降幂与组合数相乘能把底数从求和变量换成常量,结果很好看: $$ \begin{aligned} x^k &= \sum_{i=0}^{k} \begin{Bmatrix} k\\i \end{Bmatrix}x^{\underline i}\ \binom{n}{x}x^{\underline k} &= \binom{n-k}{x-k}n^{\underline k} \end{aligned} $$

剩下的过程就非常顺畅了,一步到位:

$$ \begin{aligned} &\sum_{i=0}^{n} i^k \binom{n}{i} \left( \left \lceil \frac{m}{2} \right \rceil \right)^i \left( \left \lfloor \frac{m}{2} \right \rfloor \right)^{n-i}\ = &\sum_{i=0}^n \binom{n}{i} \left( \left \lceil \frac{m}{2} \right \rceil \right)^i \left( \left \lfloor \frac{m}{2} \right \rfloor \right)^{n-i} \sum_{j=0}^k \begin{Bmatrix} k\\j \end{Bmatrix}i^{\underline j}\ = &\sum_{i=0}^n \sum_{j=0}^k \begin{Bmatrix} k\\j \end{Bmatrix} \binom{n-j}{i-j} n^{\underline j} \left( \left \lceil \frac{m}{2} \right \rceil \right)^i \left( \left \lfloor \frac{m}{2} \right \rfloor \right)^{n-i}\ = &\sum_{j=0}^k \begin{Bmatrix} k\\j \end{Bmatrix} n^{\underline j} \sum_{i=0}^n \binom{n-j}{n-i} \left( \left \lceil \frac{m}{2} \right \rceil \right)^i \left( \left \lfloor \frac{m}{2} \right \rfloor \right)^{n-i}\ = &\sum_{j=0}^k \begin{Bmatrix} k\\j \end{Bmatrix} n^{\underline j} \sum_{i=0}^{n-j} \binom{n-j}{i} \left( \left \lceil \frac{m}{2} \right \rceil \right)^{n-i} \left( \left \lfloor \frac{m}{2} \right \rfloor \right)^{i}\ = &\sum_{j=0}^k \begin{Bmatrix} k\\j \end{Bmatrix} n^{\underline j} \left( \left \lceil \frac{m}{2} \right \rceil \right)^{j}m^{n-j}\ \end{aligned} $$

最后一步将 $n-i$ 替换为 $i$ 并使用了二项式定理化简。现在我们就可以做到 $O(k^2)$ 预处理第二类斯特林数,$O(k)$ 查询了(注意不要写成 $O(k \log n)$ 查询,会T)。

代码:

#include <bits\/stdc++.h>
using namespace std;

typedef long long ll;
const int Mod = 998244353;
const int MAXN = 2005;

ll n, m, k;
ll S[MAXN][MAXN];

inline int qpow(int a, int b) {
    ll base = a, ans = 1;
    while (b) {
        if (b & 1) ans = ans * base % Mod;
        base = base * base % Mod;
        b >>= 1;
    }
    return ans;
}

void solve() {
    scanf("%d%d%d", &n, &m, &k);

    int ans = 0;
    ll a1 = 1, a2 = 1, a3 = qpow(m, n), inv = qpow(m, Mod - 2);
    for (int j = 1; j <= min(k, n); j++) {
        a1 = a1 * ((m + 1) \/ 2) % Mod;
        a2 = a2 * (n - j + 1) % Mod;
        a3 = a3 * inv % Mod;
        ans = (ans + S[k][j] * a1 % Mod * a2 % Mod * a3) % Mod;
    }
    printf("%d\n", ans);
}

int main() {
    S[0][0] = 1;
    for (int i = 1; i < MAXN; i++)
        for (int j = 1; j <= i; j++)
            S[i][j] = (S[i - 1][j - 1] + j * S[i - 1][j]) % Mod;
    
    int T;
    scanf("%d", &T);
    while (T--) {
        solve();
    }
    return 0;
}

CF1715E Long Way Home 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2022-08-22 23:41:51

洛谷传送门: CF1715E Long Way Home


个人感觉不错的一道图论+dp的题目


首先应当想到从城市 $1$ 到 $i$ 的路程必然可以看作是先走若干条(可能是零条)道路,然后乘坐一次航班,再走若干条道路,然后再次乘坐一次航班……如此循环不超过 $k$ 次得到。其次注意到 $k \le 20$ 的数据范围,不难考虑到可以通过某些算法维护一轮行动,然后直接暴力维护 $k$ 轮即可。

记 $\{dis_i\}$ 为上一轮结束后从 $1$ 到 $i$ 点的最短路,初始 $dis_1=0$,其余全为 $+\infty$。

首先考虑走若干条道路的部分,建立超级源点 $S$,从 $S$ 向点 $i$ 连边权为 $dis_i$ 的有向边,然后跑一遍 $\text{Dijkstra}$ 即可,这是图论中的常见套路,比较显然。

然后考虑 $\text{dp}$ 以维护乘坐航班的部分,我们有:

$$ dis'i = \min{1 \le j \le n} \{dis_j+(i-j)^2\} $$

不妨令 $j \le i$ ,对于 $j > i$ 部分的贡献可以倒着再做一遍 $\text{dp}$,发现这就是经典斜率优化的式子:

$$ 2ij+dis'_i-i^2=dis_j+j^2 $$

单调队列维护下凸包即可。

代码:

\/\/CF1715E Long Way Home
#include <bits\/stdc++.h>
using namespace std;

typedef long long ll;
const int MAXN = 1e5+5;
const ll INF = 0x3fffffffffffffff;

int n, m, k;
ll dis[MAXN], f[MAXN];
bool flag[MAXN];

vector<pair<int, ll> > G[MAXN];
priority_queue<pair<ll, int> > Q;

void dijkstra() {
    memset(dis, 0x3f, sizeof(dis));
    memset(flag, 0, sizeof(flag));
    dis[0] = 0;
    Q.push(make_pair(0, 0));
    while (!Q.empty()) {
        int top = Q.top().second;
        Q.pop();
        if (flag[top]) continue;
        flag[top] = 1;
        for (auto to : G[top])
            if (dis[to.first] > dis[top] + to.second) {
                dis[to.first] = dis[top] + to.second;
                Q.push(make_pair(-dis[to.first], to.first));
            }
    }
}

int Q1[MAXN];
int head, tail;

inline ll X(int x) { return x; }
inline ll Y(int x) { return dis[x] + (ll) x * x; }
inline long double slope(int a, int b) { return (long double) (Y(b) - Y(a)) \/ (X(b) - X(a)); }

inline void dp() {
    head = 1, tail = 0;
    Q1[++tail] = 1;
    for (int i = 2; i <= n; i++) {
        while (head < tail && slope(Q1[head], Q1[head + 1]) < 2 * i) head++;
        f[i] = min(dis[i], dis[Q1[head]] + (ll) (i - Q1[head]) * (i - Q1[head]));
        while (head < tail && slope(i, Q1[tail]) < slope(i, Q1[tail - 1])) tail--;
        Q1[++tail] = i;
    }

    head = 1, tail = 0;
    Q1[++tail] = n;
    for (int i = n - 1; i; i--) {
        while (head < tail && slope(Q1[head], Q1[head + 1]) > 2 * i) head++;
        f[i] = min(f[i], dis[Q1[head]] + (ll) (i - Q1[head]) * (i - Q1[head]));
        while (head < tail && slope(i, Q1[tail]) > slope(i, Q1[tail - 1])) tail--;
        Q1[++tail] = i;
    }

    for (auto &to : G[0]) to.second = f[to.first];
}

void solve() {
    scanf("%d%d%d", &n, &m, &k);
    for (int i = 1; i <= m; i++) {
        int u, v, x;
        scanf("%d%d%d", &u, &v, &x);
        G[u].emplace_back(v, x);
        G[v].emplace_back(u, x);
    }

    G[0].emplace_back(1, 0);
    for (int i = 2; i <= n; i++) G[0].emplace_back(i, INF);
    
    for (int i = 1; i <= k; i++) {
        dijkstra();
        dp();
    }

    dijkstra();

    for (int i = 1; i <= n; i++) printf("%lld ", dis[i]);
}

int main() {
    solve();
    return 0;
}
共 23 篇博客