2024.3 武汉集训做题笔记

2024.3 武汉集训做题笔记

数据结构

CF983E NN country

没啥意思,每个点一定跳到能往上走的最上的点,先跳到还有一步能到 lca 时跳到的最远地方,设 \(x,y\) 分别跳到了 \(u,v\),则最后判断是否有一条路径同时覆盖 \(u,v\),如果有则还需一步,否则要两步

这就是二维数点,扫描线,向上跳由于每个点每次跳一步的方案固定,可以倍增,时间复杂度 \(O(n\log n)\)

code
struct qry  {int l, r, id;};    vector<qry> ask[N];
struct BIT
{
    int sum[N];
    void upd(int x, int val)    {for(; x <= n; x += x & (-x))   sum[x] += val;}
    int query(int x)    {int res = 0;   for(; x; x -= x & (-x)) res += sum[x];  return res;}
    int ask(int l, int r)   {return query(r) - query(l - 1);}
}bit;
void dfs(int x)
{
    dep[x] = dep[fa[x]] + 1, dfn[x] = ++cnt, rmq[0][cnt] = fa[x]; 
    for(int y : edge[x])    dfs(y);
    mxdfn[x] = cnt;
}0
int getlca(int x, int y)
{
    if(dfn[x] > dfn[y]) swap(x, y);
    int l = dfn[x] + 1, r = dfn[y], v = lg[r - l + 1];
    return dfn[rmq[v][l]] < dfn[rmq[v][r - (1 << v) + 1]] ? rmq[v][l] : rmq[v][r - (1 << v) + 1];
}
void Dfs(int x)
{
    for(int y : edge[x])
    {
        Dfs(y);
        if(dep[f[0][y]] < dep[x] && dep[f[0][y]] < dep[f[0][x]])    f[0][x] = f[0][y];
    }
}
pii jump(int x, int y)
{
    int res = 0;
    for(int i = 17; i >= 0; --i)
        if(f[i][x] && dep[f[i][x]] > dep[y])    x = f[i][x], res += 1 << i;
    return {x, res};
}
int main()
{
    #ifdef Kelly
        freopen("in.txt", "r", stdin);
        freopen("out.txt", "w", stdout);
        freopen("err.txt", "w", stderr);
    #endif
    read(n), lg[0] = -1;
    for(int i = 2; i <= n; ++i) read(fa[i]), edge[fa[i]].pb(i);
    for(int i = 1; i <= n; ++i) lg[i] = lg[i >> 1] + 1;
    dfs(1), dep[0] = N;
    for(int j = 1; j <= 17; ++j)
        for(int i = 1; i + (1 << j) - 1 <= n; ++i)
        {
            int ln = rmq[j - 1][i], rn = rmq[j - 1][i + (1 << (j - 1))];
            rmq[j][i] = dfn[ln] < dfn[rn] ? ln : rn;
        }
    read(m);
    for(int i = 1; i <= m; ++i) 
    {
        read(u, v);
        if(dfn[u] > dfn[v]) swap(u, v);
        int lca = getlca(u, v);
        if(dep[lca] < dep[u] && dep[f[0][u]] > dep[lca])   f[0][u] = lca;
        if(dep[lca] < dep[v] && dep[f[0][v]] > dep[lca])   f[0][v] = lca;
        upd[dfn[u]].pb(dfn[v]);
    }
    Dfs(1);
    for(int j = 1; j <= 17; ++j)
        for(int i = 1; i <= n; ++i) f[j][i] = f[j - 1][f[j - 1][i]];
    read(q);
    for(int i = 1; i <= q; ++i)
    {
        read(u, v);
        int lca = getlca(u, v);
        if(dfn[u] < dfn[v]) swap(u, v); 
        pii x = jump(u, lca), y = jump(v, lca);
        if(v == lca)    ans[i] = dep[f[0][x.fi]] <= dep[v] ? x.se + y.se + 1 : -1;
        else  
        {
            ans[i] += x.se + y.se + 2;
            if(dep[f[0][x.fi]] > dep[lca] || dep[f[0][y.fi]] > dep[lca])    ans[i] = -1;
            else    ask[dfn[y.fi] - 1].pb({dfn[x.fi], mxdfn[x.fi], -i}), ask[mxdfn[y.fi]].pb({dfn[x.fi], mxdfn[x.fi], i});
        }
    }
    for(int i = 1; i <= n; ++i)
    {
        for(int x : upd[i]) bit.upd(x, 1);
        for(qry x : ask[i]) x.id > 0 ? tot[x.id] += bit.ask(x.l, x.r) : tot[-x.id] -= bit.ask(x.l, x.r);
    }
    for(int i = 1; i <= q; ++i) print(ans[i] - (tot[i] > 0 ? 1 : 0)), putchar('\n');
    return 0;
}

CF1578B Building Forest Trails

断环为链,发现一个连通块的连边,可以简化为只有相邻的点连边,容易发现不会影响相交情况

我们设一个点 \(x\) 上方覆盖的边数为 \(h_x\)(不包括端点刚好是 \(x\) 的边)

每次连边 \(u,v\),从 \(u,v\) 处不断向中间靠近,设 \(h_u > h_v\)\(u\) 右边第一个 \(h_x < h_u\) 的地方为 \(x\),则 \(u\)\(x\) 连边后一定在同一个连通块,把它们合并

直到当 \(h_u = h_v\) 时,如果 \(u,v\) 中间还有一个 \(h\) 更小的位置,那么它也在这个连通块中,合并,否则直接合并 \(u,v\) 并结束整个过程

合并同时需要维护每个点的 \(h\),使用线段树维护,并查集维护连通块的同时记录每个连通块最靠左的点和最靠右的点,如果两个连通块不交,则它们最靠左的点和最靠右的点有连边,中间点的 \(h\) 增加 \(1\),否则相交部分的 \(h\) 减少 \(1\)

总共只会发生 \(O(n)\) 次合并,复杂度 \(O(n\log n)\)

code
struct DSU
{
    int fa[N], lef[N], rit[N];
    int find(int x) {return x != fa[x] ? fa[x] = find(fa[x]) : x;}
    void init() {for(int i = 1; i <= n; ++i)    fa[i] = lef[i] = rit[i] = i;}
    void merge(int x, int y)  
    {
        x = find(x), y = find(y);
        if(x != y)  lef[y] = min(lef[y], lef[x]), rit[y] = max(rit[y], rit[x]), fa[x] = y;
    }
}dsu;
struct segtree
{
    int mn[N << 2], tag[N << 2];
    int ls(int x)   {return x << 1;}
    int rs(int x)   {return x << 1 | 1;}
    void pushup(int x)  {mn[x] = min(mn[ls(x)], mn[rs(x)]);}
    void upd(int x, int val)    {mn[x] += val, tag[x] += val;}
    void pushdown(int x)    {if(tag[x] != 0)    upd(ls(x), tag[x]), upd(rs(x), tag[x]), tag[x] = 0;}
    void update(int l, int r, int val, int nl, int nr, int p)
    {
        if(l > r)   return;
        if(l <= nl && nr <= r)  return upd(p, val);
        pushdown(p);
        int mid = (nl + nr) >> 1;
        if(mid >= l)    update(l, r, val, nl, mid, ls(p));
        if(mid < r) update(l, r, val, mid + 1, nr, rs(p));
        pushup(p);
    }
    int findright(int x, int val, int l, int r, int p) // right of x  < val
    {
        if(mn[p] >= val)    return x - 1;
        if(l == r)  return mn[p] < val ? l : x - 1;
        int mid = (l + r) >> 1;
        pushdown(p);
        int res = x - 1;
        if(mid > x && mn[ls(p)] < val)  res = findright(x, val, l, mid, ls(p));
        if(res <= x)    res = findright(x, val, mid + 1, r, rs(p));
        return res;
    }
    int findleft(int x, int val, int l, int r, int p)  
    {
        if(mn[p] >= val) return x + 1;
        if(l == r)  return mn[p] < val ? l : x + 1;
        int mid = (l + r) >> 1;
        pushdown(p);
        int res = x + 1;
        if(mid + 1 < x && mn[rs(p)] < val)  res = findleft(x, val, mid + 1, r, rs(p));
        if(res >= x)    res = findleft(x, val, l, mid, ls(p));
        return res;
    }
    int query(int pos, int l, int r, int p) 
    {
        if(l == r)  return mn[p];
        pushdown(p);
        int mid = (l + r) >> 1;
        if(mid >= pos)  return query(pos, l, mid, ls(p));
        return query(pos, mid + 1, r, rs(p));
    }
}tree;
void join(int x, int y)
{
    int u = dsu.find(x), v = dsu.find(y);
    if(dsu.lef[u] > dsu.lef[v]) swap(x, y), swap(u, v);
    if(dsu.rit[u] < dsu.lef[v])    tree.update(dsu.rit[u] + 1, dsu.lef[v] - 1, 1, 1, n, 1);
    else    tree.update(dsu.lef[v], min(dsu.rit[v], dsu.rit[u]), -1, 1, n, 1);
    dsu.merge(u, v);
}
int main()
{
    #ifdef Kelly
        freopen("in.txt", "r", stdin);
        freopen("out.txt", "w", stdout);
        freopen("err.txt", "w", stderr);
    #endif
    read(n, m);
    dsu.init();
    for(int i = 1; i <= m; ++i)
    {
        read(op, u, v);
        if(u > v)   swap(u, v);
        if(op == 1)
        {
            int x = tree.query(u, 1, n, 1), y = tree.query(v, 1, n, 1);
            while(dsu.find(u) != dsu.find(v))
                if(x > y)   join(u, tree.findright(u, x, 1, n, 1)), --x;
                else if(x < y)  join(tree.findleft(v, y, 1, n, 1), v), --y;
                else
                {
                    int pos = tree.findright(u, x, 1, n, 1);
                    if(pos < v && pos > u)  join(u, pos), --x;
                    else    {join(u, v);    break;}
                }
        }
        else    putchar(dsu.find(u) != dsu.find(v) ? '0' : '1');
        // cerr << i << "\n";
    }
    return 0;
}

CF765F Souvenirs

支配对好题

考虑相邻的三个数 \(a,b,c\),只有 \(|a-c| < |a-b|\)\(|a-c| < |b - c|\)\((a,c)\) 才有用

先考虑右边数比左边大的对,右边比左边小的再反过来做一遍

于是发现 \(c - a < b - a\)\(c < b\)\(b-c > c - a\),得 \(c < \frac{a+b}{2}\),则 \(c-a\) 至多是 \(b-a\) 的一半

两个数之间的差每次都小一半,则产生贡献的对一共只有 \(O(n\log V)\) 对,把它们找出来后,扫描线回答询问即可

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

code
inline int update(int pre, int id, int val, int l, int r)
{
	int p = ++idx;
	ls[p] = ls[pre], rs[p] = rs[pre], sum[p] = min(sum[pre], val);
	if(l == r)	return p;
	int mid = (l + r) >> 1;
	if(mid >= id)	ls[p] = update(ls[pre], id, val, l, mid);
	else	rs[p] = update(rs[pre], id, val, mid + 1, r);
	return p;
}
inline int query(int nw, int l, int r, int nl, int nr)
{
	if(l <= nl && nr <= r)	return sum[nw];
	int mid = (nl + nr) >> 1, res = inf;
	if(mid >= l)	res = min(res, query(ls[nw], l, r, nl, mid));
	if(mid < r)	res = min(res, query(rs[nw], l, r, mid + 1, nr));
	return res;
}
inline void workup(int x)
{
	nxt = query(root[x + 1], a[x] + 1, inf, 1, inf);
	if(nxt > n)	return;
	V = a[nxt] - a[x] + 1, point[x].pb(nxt);
	while(V > 1)
	{
		bef = nxt;
		if(bef >= n)	return;
		nxt = query(root[bef + 1], a[x] + 1, a[x] + (V >> 1) - 1 + 1, 1, inf);
		if(nxt > n)	return;
		point[x].pb(nxt), V = a[nxt] - a[x] + 1;
	}
}
inline void workdown(int x)
{
	nxt = query(root[x + 1], 1, a[x] + 1, 1, inf);
	if(nxt > n)	return;
	V = a[x] - a[nxt] + 1, point[x].pb(nxt);
	while(V > 1)
	{
		bef = nxt;
		if(bef >= n)	return;
		nxt = query(root[bef + 1], a[x] - (V >> 1) + 1 + 1, a[x] + 1, 1, inf);
		if(nxt > n)	return;
		point[x].pb(nxt), V = a[x] - a[nxt] + 1;
	}
}
inline void add(int x, int val)
{
	while(x <= n)	tree[x] = min(tree[x], val), x += x & (-x);
}
inline int ask(int x)
{
	int res = inf;
	while(x)	res = min(res, tree[x]), x -= x & (-x);
	return res;
}
int main()
{
	n = read();
	for(reg int i = 1; i <= n; ++i)	a[i] = read();
	memset(sum, 0x3f, sizeof(sum)), memset(tree, 0x3f, sizeof(tree));
	for(reg int i = n; i > 0; --i)	root[i] = update(root[i + 1], a[i] + 1, i, 1, inf);
	for(reg int i = 1; i < n; ++i)	workup(i), workdown(i);
	m = read();
	for(reg int i = 1; i <= m; ++i)
	{
		li = read(), ri = read();
		vec[li].pb(mp(ri, i));
	}
	for(reg int i = n - 1; i > 0; --i)
	{
		for(reg int j : point[i])
		    add(j, abs(a[j] - a[i]));
		for(reg pii j : vec[i])
		    ans[j.se] = ask(j.fi);
	}
	for(reg int i = 1; i <= m; ++i)	print(ans[i]), putchar('\n');
	return 0;
}

CF1209G2 Into Blocks (hard version)

考虑 easy version 的做法,只用求出全局答案,考虑将序列在能断开的位置断开,即没有颜色跨过这个位置,则每个段中的颜色必须相同,全部改成众数即可

现在要支持单点修改,要找出每个段及段中出现次数最多的颜色,很神仙的想法是找出每个颜色最左边和最右边的点 \(l,r\),将 \([l,r-1]\) 加上 \(1\),设此时 \(i\) 处的和为 \(h_i\),则每个和为 \(0\) 的位置能断开,而且根据段的定义一种颜色肯定全部出现在某段内,把它的大小放到 \(l\) 处,每段的贡献则是段长减去段内的最大值

线段树维护这个序列,每次要进行区间加减,由于和最小就是 \(0\),相当于区间在最小值处断开,合并两个区间时,分类讨论它们最小值的大小关系,维护区间内已经包含的段的最大值的和,左端点,右端点处未封闭的段的最大值,整个区间内的最大值,还有 \(h_i\) 的最小值

复杂度 \(O(n\log n)\)

code
struct segtree
{
	int sum[N << 2], mn[N << 2], tag[N << 2], lm[N << 2], rm[N << 2], mx[N << 2];
	int lson(int x)	{return x << 1;}
	int rson(int x)	{return x << 1 | 1;}
	void pushup(int x)
	{
        mn[x] = min(mn[lson(x)], mn[rson(x)]), mx[x] = max(mx[lson(x)], mx[rson(x)]);
		/*if(mn[lson(x)] && mn[rson(x)]) // no 0
		{
			sum[x] = mx[x], rm[x] = lm[x] = 0;
		}
		else*/ if(mn[lson(x)] > mn[rson(x)]) // 0 in right
		{
			lm[x] = max(mx[lson(x)], lm[rson(x)]), rm[x] = rm[rson(x)];
			sum[x] = sum[rson(x)];
		}
		else if(mn[rson(x)] > mn[lson(x)]) // 0 in left
		{
			lm[x] = lm[lson(x)], rm[x] = max(rm[lson(x)], mx[rson(x)]);
			sum[x] = sum[lson(x)];
		}
		else // both have 0
		{
			sum[x] = sum[lson(x)] + sum[rson(x)] + max(rm[lson(x)], lm[rson(x)]);
			lm[x] = lm[lson(x)], rm[x] = rm[rson(x)];
		}
	}
	void pushdown(int x)
	{
		if(!tag[x])	return;
        tag[lson(x)] += tag[x], tag[rson(x)] += tag[x];
        mn[lson(x)] += tag[x], mn[rson(x)] += tag[x];
        tag[x] = 0;
	}
	void upd(int l, int r, int val, int nl, int nr, int p) // [l,r] + val
    {
        if(l <= nl && nr <= r)  
        {
            mn[p] += val, tag[p] += val;
            return;
        }
        int mid = (nl + nr) >> 1;
        pushdown(p);
        if(mid >= l)    upd(l, r, val, nl, mid, lson(p));
        if(mid < r) upd(l, r, val, mid + 1, nr, rson(p));
        pushup(p);
    }
	void modify(int id, int val, int l, int r, int p) // sum[id] + val
    {
        if(l == r)  {lm[p] += val, mx[p] += val; return;}
        int mid = (l + r) >> 1;
        pushdown(p);
        if(mid >= id)   modify(id, val, l, mid, lson(p));
        else    modify(id, val, mid + 1, r, rson(p));
        pushup(p);
    }
}tree;
void update(int tmpl, int tmpr, int nwl, int nwr)
{
	if(nwl != tmpl)
	{
		if(nwl > tmpl)	tree.upd(tmpl, nwl - 1, -1, 1, n, 1);
		else	tree.upd(nwl, tmpl - 1, 1, 1, n, 1);
	}
	else if(nwr != tmpr)
	{
		if(nwr > tmpr)	tree.upd(tmpr, nwr - 1, 1, 1, n, 1);
		else	tree.upd(nwr, tmpr - 1, -1, 1, n, 1);
	}
}
int main()
{
	read(n, q);
	for(int i = 1; i <= n; ++i)
	{
		read(a[i]);
		cpos[a[i]].insert(i);
	}
	for(int i = 1; i <= n; ++i)	++num[pre[a[i]]];
	for(int i = 1; i <= V; ++i)
		if(cpos[i].size())
        {
            if(cpos[i].size() > 1)  tree.upd(*cpos[i].begin(), *cpos[i].rbegin() - 1, 1, 1, n, 1);
            tree.modify(*cpos[i].begin(), cpos[i].size(), 1, n, 1);
        }
    print(n - tree.sum[1] - tree.lm[1] - tree.rm[1]), putchar('\n');
	for(int i = 1; i <= q; ++i)
	{
		read(u, v);
		int tmpl = *cpos[a[u]].begin(), tmpr = *cpos[a[u]].rbegin(), nwl, nwr;
		tree.modify(tmpl, -cpos[a[u]].size(), 1, n, 1), cpos[a[u]].erase(u);
        if(!cpos[a[u]].empty())
        {
            nwl = *cpos[a[u]].begin(), nwr = *cpos[a[u]].rbegin();
            update(tmpl, tmpr, nwl, nwr), tree.modify(nwl, cpos[a[u]].size(), 1, n, 1);
        }
		if(!cpos[v].empty())
        {
            tmpl = *cpos[v].begin(), tmpr = *cpos[v].rbegin();
            tree.modify(tmpl, -cpos[v].size(), 1, n, 1), cpos[v].insert(u);
            nwl = *cpos[v].begin(), nwr = *cpos[v].rbegin();
            update(tmpl, tmpr, nwl, nwr), tree.modify(nwl, cpos[v].size(), 1, n, 1);
        }
		else    cpos[v].insert(u), tree.modify(u, 1, 1, n, 1);
		a[u] = v;
		print(n - tree.sum[1] - tree.lm[1] - tree.rm[1]), putchar('\n'); 
	}
	return 0;
}

[AGC001F] Wide Swap

首先 \(j-i\ge k\) 的限制太难处理了,就令序列 \(a_{p_i}=i\),操作就变成了每次交换 \(a\) 中相邻的两个数,但要求差值 \(\ge k\),希望最小化这个排列的字典序

交换相邻两数相当于冒泡排序,我们考虑差 \(>k\) 的一对数,它们的相对位置不会改变,剩下数的相对位置可以随意交换

这样有限制的对中,前面的数向后面连边,即要求 DAG 字典序最小的拓扑序

这里要注意,求字典序最小的拓扑序,应该是把图上边反转后,求字典序最大的拓扑序,再将序列翻转

不过本题中由于连边方式特殊,两种求出来都一样

我们用线段树维护每个点的入度,由于入度最小是 \(0\),在线段树上找到最靠后的最小值 \(x\),取出,然后将 \(x+k+1\sim n\) 的入度减去 \(1\)

注意这里可能有数在 \(x\) 所在位置前面,应该是有它到 \(x\) 的边而不是 \(x\) 到它的,但是不要紧,它肯定被先取出了,把取出的数入度设为 \(\infty\) 即可

code
struct BIT
{
    int sum[N];
    void upd(int x, int val)    {for(; x <= n; x += x & (-x))   sum[x] += val;}
    int qry(int x)  {int res = 0;   for(; x; x -= x & (-x)) res += sum[x];  return res;}
    int ask(int l, int r)   {l = max(l, 1), r = min(r, n);  return l > r ? 0 : qry(r) - qry(l - 1);}
}bit;
struct segtree
{
    int mn[N << 2], tag[N << 2];
    int ls(int x)   {return x << 1;}
    int rs(int x)   {return x << 1 | 1;}
    void pushup(int x)  {mn[x] = min(mn[ls(x)], mn[rs(x)]);}
    void upd(int x, int val)    {mn[x] += val, tag[x] += val;}
    void pushdown(int x)    {if(tag[x] != 0)    upd(ls(x), tag[x]), upd(rs(x), tag[x]), tag[x] = 0;}
    void build(int l, int r, int p) 
    {
        if(l == r)  return void(mn[p] = deg[l]);
        int mid = (l + r) >> 1;
        build(l, mid, ls(p)), build(mid + 1, r, rs(p));
        pushup(p);
    }
    void update(int l, int r, int val, int nl, int nr, int p)   
    {
        if(l <= nl && nr <= r)  return upd(p, val);
        int mid = (nl + nr) >> 1;
        pushdown(p);
        if(mid >= l)    update(l, r, val, nl, mid, ls(p));
        if(mid < r) update(l, r, val, mid + 1, nr, rs(p));
        pushup(p);
    }
    int find(int l, int r, int p)   
    {
        if(l == r)  return mn[p] = inf, l;
        int mid = (l + r) >> 1, res = 0;
        pushdown(p);
        if(mn[rs(p)] <= 0)  res = find(mid + 1, r, rs(p));
        else    res = find(l, mid, ls(p));
        pushup(p);  return res;
    }
}tree;
int main()
{
    read(n, k);
    for(int i = 1; i <= n; ++i) read(p[i]), a[p[i]] = i;
    for(int i = n; i > 0; --i)
    {
        deg[a[i]] = bit.ask(a[i] - k + 1, a[i] + k - 1);
        bit.upd(a[i], 1);
    }
    tree.build(1, n, 1);
    for(int i = 1; i <= n; ++i)
    {
        int x = tree.find(1, n, 1);
        ans[n - i + 1] = x;
        tree.update(max(1, x - k + 1), min(n, x + k - 1), -1, 1, n, 1);
    }
    for(int i = 1; i <= n; ++i) p[ans[i]] = i;
    for(int i = 1; i <= n; ++i) print(p[i]), putchar('\n');
    return 0;
}

动态规划

[AGC007E] Shik and Travel

行走的限制相当于一次必须走完整棵子树才能出去

显然可以二分答案,考虑最朴素的 DP ,设 \(f(x,a,b)\) 表示当进入 \(x\) 到叶子的路径长为 \(a\),从叶子出去的路径长为 \(b\) 时是否可行

发现当 \(a' > a\)\(b' > b\)\((a',b')\) 完全没用了,即 \(a\) 升序排序时 \(b\) 降序

但复杂度看起来还是爆炸

合并两棵子树,先考虑走左子树再走右子树的情况,\(a\) 已经固定了,找到 \(b+a' < lim\) 的最大的 \(a'\),此时它对应的 \(b'\) 最小,即作为新的 \(b\),那么发现有用的 \((a,b)\) 对其实只有 \(\min siz\) 对,复杂度分析就同启发式合并,是 \(O(n\log n)\)

合并时由于 \(a,b\) 均有单调性,因此可以双指针加归并排序,注意去重,总复杂度 \(O(n\log n\log V)\)

code
vector<pll> merge(vector<pll> &x, vector<pll> &y, ll u, ll v, int op)
{
    vector<pll> z;
    if(op)  swap(x, y), swap(u, v);
    for(int i = 0, j = 0, lst = 0; i < x.size(); lst = j, ++i)
    {
        while(j < y.size() && y[j].fi + x[i].se + u + v <= lim)    ++j;
        if(j > lst) z.pb({x[i].fi + u, y[j - 1].se + v});
    }
    return z;
}
void gb(vector<pll> &x, vector<pll> &y)
{
    vector<pll> z;
    for(int i = 0, j = 0; i < x.size() || j < y.size(); )
        if(i < x.size() && (j >= y.size() || x[i] < y[j])) z.pb(x[i++]);
        else    z.pb(y[j++]);
    x.clear();
    for(pll i : z)  
        if(x.empty() || i.fi > x.back().fi) x.pb(i);
}
void dfs(int x)
{
    if(!num[x]) return void(f[x].pb({0, 0}));
    dfs(son[x][0]), dfs(son[x][1]);
    f[x] = merge(f[son[x][0]], f[son[x][1]], a[x][0], a[x][1], 0);
    vector<pll> tmp = merge(f[son[x][0]], f[son[x][1]], a[x][0], a[x][1], 1);
    gb(f[x], tmp);
}
int check(ll x)
{
    lim = x;
    for(int i = 1; i <= n; ++i) f[i].clear();
    dfs(1);
    return f[1].empty() ? 2 : 1;
}
int main()
{
    read(n);
    for(int i = 2; i <= n; ++i)
    {
        read(u, v);
        son[u][num[u]] = i, a[u][num[u]++] = v;
    }
    ll l = 0, r = inf;
    while(l < r)
    {
        ll mid = (l + r) >> 1;
        if(check(mid) == 1) r = mid;
        else    l = mid + 1;
    }
    cout << l;
    return 0;
}

CF379G New Year Cactus

点仙人掌上背包,一条边上不能有不同物品,注意是点仙人掌,要建圆方树

在方点处直接递归进入子节点,在圆点时,如果它子节点中有方点,说明这是一个环,把这个环对应的节点全部找出来,合并背包,注意断环为链的地方首尾相接也要不同,因此枚举开头,只统计链结尾合法的方案

复杂度依然是树形背包,\(O(n^2)\)

code
void dfs(int x, int fa)
{
    dfn[x] = low[x] = ++cnt, stk[++top] = x;
    for(int y : edge[x])
        if(!dfn[y]) 
        {
            dfs(y, x), low[x] = min(low[x], low[y]);
            if(low[y] >= dfn[x])
            {
                ++idx, col[x] = idx, cut[x] = 1;
                while(top && stk[top + 1] != y) col[stk[top]] = idx, cir[idx].pb(stk[top--]);
                cir[idx].pb(x), reverse(cir[idx].begin(), cir[idx].end());
            }
        }
        else if(dfn[y] < dfn[x] && y != fa) low[x] = min(low[x], dfn[y]);
}
void merge(int x, int y)
{
    for(int i = 0; i <= siz[x] + siz[y]; ++i)   tmp[i][0] = tmp[i][1] = tmp[i][2] = -inf;
    for(int i = 0; i <= siz[x]; ++i)
        for(int j = 0; j <= siz[y]; ++j)
        {
            tmp[i + j][0] = max(tmp[i + j][0], f[x][i][0] + max(f[y][j][0], f[y][j][2]));
            tmp[i + j][1] = max(tmp[i + j][1], f[x][i][1] + max(f[y][j][1], f[y][j][2]));
            tmp[i + j][2] = max(tmp[i + j][2], f[x][i][2] + max({f[y][j][0], f[y][j][1], f[y][j][2]}));
        }
    for(int i = 0; i <= siz[x] + siz[y]; ++i)    f[x][i][0] = tmp[i][0], f[x][i][1] = tmp[i][1], f[x][i][2] = tmp[i][2];
}
void work(int u)
{
    int nw = cir[u].back();
    for(int x : cir[u]) memcpy(g[x], f[x], sizeof(g[x]));
    for(int i = 0; i <= siz[nw]; ++i)
        for(int j = 0; j < 3; ++j)  f[nw][i][j] = -inf;
    for(int i = 0; i <= siz[u] + siz[cir[u][0]]; ++i)
        for(int j = 0; j < 3; ++j)  mx[i][j] = -inf;
    for(int i = 0; i < 3; ++i)
    {
        for(int j = 0; j <= siz[nw]; ++j)    f[nw][j][i] = g[nw][j][i];
        for(int j = (int)cir[u].size() - 2; j >= 0; --j)
            merge(cir[u][j], cir[u][j + 1]), siz[cir[u][j]] += siz[cir[u][j + 1]];
        for(int j = 0; j <= siz[cir[u][0]]; ++j)
            for(int k = 0; k < 3; ++k) 
                if(i == k || i == 2 || k == 2)  mx[j][k] = max(mx[j][k], f[cir[u][0]][j][k]);
        for(int j = 0; j + 1 < cir[u].size(); ++j)  siz[cir[u][j]] -= siz[cir[u][j + 1]];
        for(int x : cir[u]) 
            for(int j = 0; j <= siz[x]; ++j)  
                for(int k = 0; k < 3; ++k)  f[x][j][k] = g[x][j][k];
        for(int j = 0; j <= siz[nw]; ++j)
            for(int k = 0; k < 3; ++k)  f[nw][j][k] = -inf;
    }
    for(int i = 0; i <= siz[cir[u][0]] + siz[u]; ++i)
        for(int j = 0; j < 3; ++j)  f[cir[u][0]][i][j] = mx[i][j];
}
void Dfs(int v, int fa)
{
    if(v <= n)  siz[v] = 1, f[v][1][0] = f[v][0][2] = 0, f[v][0][1] = 1;
    else
    {
        for(int x : tree[v])  
        {
            if(x == fa) continue;
            Dfs(x, v), siz[v] += siz[x];
        }
        return;
    }
    for(int u : tree[v])
        if(u != fa) 
        {
            Dfs(u, v);
            work(u), siz[v] += siz[u];
        }
}
int main()
{
    read(n, m), root = 1;
    for(int i = 1; i <= m; ++i) read(u, v), edge[u].pb(v), edge[v].pb(u);
    idx = n, dfs(root, 0);
    for(int i = n + 1; i <= idx; ++i)
        for(int j : cir[i]) tree[i].pb(j), tree[j].pb(i);
    for(int i = 1; i <= n; ++i)
        for(int j = 0; j <= n; ++j)
            for(int k = 0; k < 3; ++k)  f[i][j][k] = -inf;
    Dfs(root, 0);f
    for(int i = 0; i <= n; ++i) print(max({f[root][i][0], f[root][i][1], f[root][i][2]})), putchar(' ');
    return 0;
}

[AGC056B] Range Argmax

神秘 Ad-hoc 题

直接对 \(\{x_i\}\) 计数很困难,考虑把它唯一对应上一个排列,对排列计数就比较好做了

固定了 \(x\) 序列,从大到小加入数,把最大值放在能放的最靠左的地方,区间内只考虑完全包含在它内的询问区间,因为若不是则它的最大值肯定在这个区间外,已经确定了

枚举这个位置为 \(k\),这时对于它右侧的区间没有限制,但对于左侧的来说,如果没有任何一个区间 \([l_i,r_i]\) 完全在当前区间内且同时跨过左侧区间的最大值和 \(k\),那么 \(k\) 完全可以放在左侧区间最大值的地方,而不会影响 \(x\) 序列,即最大值还能往左放,就不合法了

于是设 \(f(l,r,k)\) 表示区间 \([l,r]\) 内的最大值在 \(k\) 时的方案数,预处理出 \([l,r]\) 内跨过 \(k\) 的询问区间中最小的左端点 \(x\),则 \([l,k-1]\) 内最大值在 \([x,k-1]\) 内,后缀和优化转移即可,时间复杂度 \(O(n^3)\)

code
int main()
{
    read(n, m);
    for(int i = 0; i <= n + 1; ++i)
        for(int j = 0; j <= n + 1; ++j)
            for(int k = 0; k <= n + 1; ++k) ml[i][j][k] = n + 1;
    for(int i = 1; i <= m; ++i) 
    {
        read(nl, nr), ++a[nl][nr];
        for(int j = nl; j <= nr; ++j)   ml[j][nl][nr] = min(ml[j][nl][nr], nl);
    }
    for(int i = 1; i <= n; ++i)
        for(int j = i; j <= n; ++j)
            for(int k = i; k > 0; --k)  ml[i][k][j] = min({ml[i][k][j], ml[i][k + 1][j], ml[i][k][j - 1]});
    for(int i = 1; i <= n; ++i) f[i][i][i] = g[i][i][i] = 1;
    for(int len = 2; len <= n; ++len)
        for(int l = 1; l + len - 1 <= n; ++l)
        {
            int r = l + len - 1;
            for(int k = l; k <= r; ++k)
                f[l][r][k] = (l < k ? g[l][k - 1][ml[k][l][r]] : 1ll) * (k < r ? g[k + 1][r][k + 1] : 1ll) % mod;
            for(int k = r; k >= l; --k) g[l][r][k] = add(g[l][r][k + 1], f[l][r][k]);
        }
    cout << g[1][n][1];
    return 0;
}

[ABC221H] Count Multiset

怎么还是想不到划分数的 DP 方式呢

考虑问题是将 \(n\) 划分为 \(i\) 个数,且划分出的每个数个数不超过 \(m\)

\(f(i,j)\) 表示当前划分出了 \(i\) 个数,和为 \(j\) 的方案数,转移可以是整体 \(+1\) 或是加上一个 \(1\),但问题是必须时刻保证 \(1\) 的个数不超过 \(m\)

因此设 \(g(i,j)\) 表示当前划分出了 \(i\) 个数,和为 \(j\) 且没有 \(1\) 的方案数,转移变成枚举放的 \(1\) 的个数,\(f(i,j)\gets g(i-k,j-k)(k\le m)\)\(g(i,j)\gets f(i,j-i)\)

用前缀和优化转移,复杂度 \(O(n^2)\)

code
int main()
{
    cin >> n >> m;
    f[0][0] = g[0][0] = s[0][0] = 1;
    for(int i = 1; i <= n; ++i)
        for(int j = i; j <= n; ++j)
        {
            Add(g[i][j], f[i][j - i]); 
            s[j - i][i] = add(s[j - i][i - 1], g[i][j]);
            Add(f[i][j], add(s[j - i][i], (i >= m + 1 ? mod - s[j - i][i - m - 1] : 0)));
        }
    for(int i = 1; i <= n; ++i) cout << f[i][n] << "\n";
    return 0;   
}

P5391 [Cnoi2019] 青染之心

真没啥意思,操作显然可以建出树,暴力背包即可,但是空间开不下

于是重链剖分,只记录重链开头的背包和全局背包,每次到一个点先利用全局的背包把轻子节点的背包算出并记录,然后递归重儿子,再递归轻儿子

时间复杂度 \(O(qV)\),空间复杂度 \(O(V\log q)\)

代码咕咕咕了

CF1442D Sum

如果每个序列是上凸的,那就能直接贪心

但如果每个序列是下凸的,那么只会有一个序列没有选满,否则把没选满的一个换成把另一选满更优

于是我们枚举这个没选满的数列选到了哪,然后把剩下的整个序列当作物品,相当于需要知道去掉一个物品后的背包值

可以分块,处理出整块的 DP 值后合并整块,暴力合并散块,但也可以分治,分治到 \([l,r]\) 时表示除了 \([l,r]\) 外其它部分已经做了背包,在递归 \([l,mid]\) 前合并 \([mid+1,r]\) 的背包值,到了叶子节点就是除了它之外其它做背包的值了

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

code
void solve(int l, int r)
{
    if(l == r)
    {
        for(int i = 0; i <= k; ++i) ans = max(ans, f[i] + s[l][k - i]);
        return;
    }
    ll mid = (l + r) >> 1, tmp[M];
    for(int i = 0; i <= k; ++i) tmp[i] = f[i];
    for(int i = mid + 1; i <= r; ++i)
        for(int j = k; j >= num[i]; --j)    f[j] = max(f[j], f[j - num[i]] + val[i]);
    solve(l, mid);
    for(int i = 0; i <= k; ++i) f[i] = tmp[i];
    for(int i = l; i <= mid; ++i)
        for(int j = k; j >= num[i]; --j)    f[j] = max(f[j], f[j - num[i]] + val[i]);
    solve(mid + 1, r);
    for(int i = 0; i <= k; ++i) f[i] = tmp[i];
}
int main()
{
    read(n, k);
    for(int i = 1; i <= n; ++i)
    {
        read(num[i]), a[i].resize(num[i] + 1), s[i].resize(num[i] + 1);
        for(int j = 1; j <= num[i]; ++j) read(a[i][j]);
        a[i].resize(k + 1), s[i].resize(k + 1);
        for(int j = 1; j <= k; ++j) s[i][j] = s[i][j - 1] + a[i][j];
        val[i] = s[i][k];
    }
    solve(1, n);
    cout << ans;
    return 0;
}

组合计数

CF1667E Centroid Probabilities

推式子数数题

不难发现大小为 \(n\) 的树有 \((n-1)!\)

考虑如果一个点子树大小 \(\le mid=\frac{n-1}2\) 怎么算,设当前子树大小为 \(n\),容斥,枚举根不是重心,即它有一棵大小 \(i > \frac{n-1}{2}\) 的子树,那么剩下的 \(n-i-1\) 个点加上根形成树即可

\[f_n=\sum_{i=mid+1}^n (i-1)!(n-i-1)!{n-1\choose i} \\ = (n-1)!\sum_{i=mid+1}^n \frac 1 i \]

再利用重心的性质,树的重心是深度最深的子树大小 \(>mid\) 的点,假设这个点为 \(x\),枚举它的子树大小为 \(i\),但是要考虑 \(x\) 会接在子树外的哪个点上,由于子树内点的编号都比 \(x\) 大,因此子树外编号比 \(x\) 小的点还是有 \(x-1\) 个,那么总方案数就是

\[\sum_{i=mid+1}^{n-x+1}f_{i}(n-i-1)!{n-x\choose x-1}(x-1) \]

显然可以卷积优化,复杂度 \(O(n\log n)\)

树的构造过程可以看作平常我们随机生成一棵树的过程,利用概率好像有 \(O(n)\) 做法,但我不会

code
int main()
{
    cin >> n, mid = (n - 1) >> 1;
    fact[0] = invf[0] = 1;
    for(ll i = 1; i <= n; ++i)  fact[i] = fact[i - 1] * i % mod;
    invf[n] = qmi(fact[n], mod - 2);
    for(ll i = n - 1; i > 0; --i)   invf[i] = invf[i + 1] * (i + 1) % mod;
    for(int i = mid + 1; i <= n; ++i)   s[i] = add(s[i - 1], invf[i] * fact[i - 1] % mod);
    for(int i = mid + 1; i <= n; ++i)   f[i] = add(fact[i - 1], mod - fact[i - 1] * s[i - 1] % mod);
    a.resize(n + 1), b.resize(n + 2);
    for(int i = mid; i < n; ++i)   a[n - i + 1] = fact[n - i - 1] * invf[i - 1] % mod * f[i] % mod;
    for(int j = 1; j <= n + 1; ++j)   b[j] = invf[n - j + 1];
    mul(a, b);
    print(f[n]), putchar(' ');
    // for(int i = 2; i <= mid + 1; ++i) brute force
    // {
    //     ll ans = 0;
    //     for(int j = mid + 1; j <= n - i + 1; ++j)   
    //         ans = add(ans, c(n - i, j - 1) * f[j] % mod * fact[n - j - 1] % mod * (i - 1) % mod);
    //     print(ans), putchar(' ');
    // }
    for(ll i = 2; i < mid + 2; ++i)    print(a[n + i + 1] * fact[n - i] % mod * (i - 1) % mod), putchar(' ');
    for(int i = mid + 2; i <= n; ++i)   print(0), putchar(' ');
    return 0;
}

#667. 【UNR #5】提问系统

\(p_r+p_b=n\),因此把式子拆成 \((n-p_b)p_b^2=np_b^2-p_b^3\)

栈的操作序列可以抽象成一棵树,要求每个点到根的链上,\(B\) 的个数在某个范围内

于是可以 DP,需要维护 \(B\)\(0,1,2,3\) 次方,设 \(f(x,i,j)\) 表示在节点 \(x\) 处,到根链上 \(B\) 个数为 \(i\) 时子树内 \(B\) 个数的 \(j\) 次方的总权值和,转移是经典的拆括号转移,细节比较多,复杂度 \(O(n^2)\)

code
void dfs(int x)
{
    int st = max(0, dep[x] - lima), ed = min(limb, dep[x]);
    for(int i = st; i <= ed; ++i)   f[x][i][0] = 1;
    for(int y : edge[x])
    {
        dep[y] = dep[x] + 1, dfs(y);
        for(int i = st; i <= ed; ++i)   tmp[i][0] = tmp[i][1] = tmp[i][2] = tmp[i][3] = 0;
        for(int i = st; i <= ed; ++i)
        {
            tmp[i][0] = add(f[y][i + 1][0], f[y][i][0]) * f[x][i][0] % mod;
            tmp[i][1] = add(add(f[y][i + 1][0], f[y][i][0])* f[x][i][1] % mod, add(f[y][i][1], f[y][i + 1][0], f[y][i + 1][1]) * f[x][i][0] % mod);
            tmp[i][2] = add(add(f[y][i + 1][0], f[y][i + 1][1] * 2ll % mod, f[y][i + 1][2], f[y][i][2]) * f[x][i][0] % mod, add(f[y][i + 1][0], f[y][i + 1][1], f[y][i][1]) * f[x][i][1] % mod * 2ll % mod, add(f[y][i + 1][0], f[y][i][0]) * f[x][i][2] % mod);
            ll nw3 = add(f[y][i + 1][0], f[y][i + 1][1] * 3ll % mod, f[y][i + 1][2] * 3ll % mod, f[y][i + 1][3], f[y][i][3]) * f[x][i][0] % mod;
            ll nw2 = add(f[y][i + 1][0], f[y][i + 1][1] * 2ll % mod, f[y][i + 1][2], f[y][i][2]) * f[x][i][1] % mod * 3ll % mod;
            ll nw1 = add(f[y][i + 1][0], f[y][i + 1][1], f[y][i][1]) * f[x][i][2] % mod * 3ll % mod, nw0 = add(f[y][i + 1][0], f[y][i][0]) * f[x][i][3] % mod;
            tmp[i][3] = add(nw3, nw2, nw1, nw0);
        }
        for(int i = st; i <= ed; ++i) 
            for(int j = 0; j < 4; ++j)  f[x][i][j] = tmp[i][j];
    }
}
int main()
{
    cin >> n >> lima >> limb;
    stk[++top] = n + 1;
    for(int i = 1; i <= n << 1; ++i)
    {
        cin >> op;
        if(op[1] == 'u')    ++idx, edge[stk[top]].pb(idx), stk[++top] = idx, mx = max(mx, top);
        else    --top;
    }
    if(lima + limb < top)   {cout << 0; return 0;}
    dfs(n + 1);
    cerr << f[n + 1][0][2] << " " << f[n + 1][0][3] << "\n";
    ans = add(1ll * f[n + 1][0][2] * n % mod, mod - f[n + 1][0][3]);
    cout << ans;
    return 0;
}

[ARC096E] Everything on It

容斥,设 \(f_i\) 表示钦定了 \(i\) 个数出现不多于 \(1\) 次的方案数,则只有剩下 \(n-i\) 个数的集合共 \(2^{n-i}\) 个可以随便选

\(i\) 个数枚举在 \(j\) 个集合中,因为还可以不出现,所以很妙的想法是增加一个集合表示不选的数,则共划分为 \(j+1\) 个集合,方案数就是 \(S(i,j)\)

\(j\) 个要选的集合中还可以放其它数,所以每个集合其实有 \(2^{n-i}\) 种,最后二项式反演,求出答案即可,递推求第二类斯特林数,复杂度 \(O(nm)\)

code
int main()
{
    cin >> n >> mod;
    s[0][0] = 1;
    for(int i = 1; i <= n + 1; ++i)
        for(ll j = 1; j <= i; ++j) s[i][j] = add(s[i - 1][j - 1], s[i - 1][j] * j % mod);
    for(int i = 0; i <= n; ++i)
    {
        c[i][0] = 1;
        for(int j = 1; j <= i; ++j) c[i][j] = add(c[i - 1][j - 1], c[i - 1][j]);
    }
    for(int i = 0; i <= n; ++i)
    {
        ll tmp = qmi(2ll, qmi(2ll, n - i, mod - 1), mod), lsh = qmi(2ll, n - i, mod) % mod;
        for(int j = 0; j <= i; ++j)
            if(i & 1)   ans = add(ans, mod - c[n][i] * s[i + 1][j + 1] % mod * tmp % mod * qmi(lsh, j, mod) % mod);
            else    ans = add(ans, c[n][i] * s[i + 1][j + 1] % mod * tmp % mod * qmi(lsh, j, mod) % mod);
    }
    cout << ans;
    return 0;
}

[AGC008E] Next or Nextnext

\(i\)\(a_i\) 连边,得到内向基环森林,将 \(i\)\(p_i\) 连边,得到了若干个置换环,则环的下一个点是原基环森林中的点往前跳最多 \(2\) 步得到

分类讨论基环树的形态,看它能生成的环有多少种

如果本身就是一个环,它可以不变,也可以和另一个同样大小的环拼成大环,拼接时有 \(len\) 种方法,注意大环不能再次拼接,同时如果环长为奇数,则可以每个点都指向后两个,得到新的环,注意新的环也不能拼接

拼接时枚举拼接的对数,计算答案

然后如果是基环树,则不在环上的点都要放到环上,环上每条边最多加一个点,发现点 \(x\) 下放挂的链只能放在 \(x\) 后方的那条边或那条边后面的一条边,最多只有 \(2\) 种放法,且放在后面的第二条边时,后面的第一条边不能放点

这样每条链的摆放都独立,只需要看能不能用第二种放法即可,复杂度 \(O(n)\)

code
void dfs(int x)
{
    dfn[x] = ++cnt, stk[++top] = x, tmp.pb(x);
    for(int y : edge[x])
        if(!dfn[y]) dfs(y);
        else if(dfn[y] <= dfn[x] && cir.empty())
        {
            for(int i = top; i > 0 && stk[i] != y; --i) cir.pb(stk[i]);
            cir.pb(y), reverse(cir.begin(), cir.end());
        }
    --top;
}
void Dfs(int x)
{
    siz[x] = 1;
    for(int y : edge[x])
        if(!vis[y]) Dfs(y), siz[x] += siz[y];
}
ll c(ll x, ll y)    {return fact[x] * invf[x - y] % mod * invf[y] % mod;}
int main()
{
    read(n);
    for(int i = 1; i <= n; ++i) read(a[i]), edge[a[i]].pb(i);
    for(int i = 1; i <= n; ++i)
        if(edge[i].size() > 2)  {cout << 0; return 0;}
    for(int i = 1; i <= n; ++i) mul[i] = 1;
    fact[0] = qzh[0] = invf[0] = 1;
    for(ll i = 1; i <= n; ++i)  fact[i] = fact[i - 1] * i % mod;
    invf[n] = qmi(fact[n], mod - 2);
    for(ll i = n - 1; i > 0; --i)   invf[i] = invf[i + 1] * (i + 1) % mod;
    for(ll i = 1; i <= n; ++i)  qzh[i] = (i & 1) ? qzh[i - 1] * i % mod : qzh[i - 1];
    for(int k = 1; k <= n; ++k)
    {
        if(dfn[k])  continue;
        int x = k;
        tmp.clear(), cir.clear(), lin.clear(), ++idx, top = 0;
        for(; !book[x]; x = a[x])  book[x] = 1;   
        dfs(x);
        for(int u : cir)    vis[u] = 1;
        for(int u : tmp)
            if(!vis[u] && edge[u].size() > 1)   {cout << 0; return 0;}
        if(tmp.size() - cir.size() > cir.size())    {cout << 0; return 0;}
        if(cir.size() == tmp.size())
        {
            ++num[cir.size()], f[idx] = (cir.size() == 1 || (cir.size() % 2 == 0)) ? 1 : 2;
            mul[tmp.size()] = mul[tmp.size()] * f[idx] % mod;
            continue;
        }
        f[idx] = 1;
        for(int u : cir)    Dfs(u);
        for(int j = 0; j < 2; ++j)
            for(int u : cir)    lin.pb(u);
        int st = 0;
        for(int i = 0; i < lin.size(); ++i)
            if(edge[lin[i]].size() > 1) {st = i;    break;}
        for(int i = st, j = i + 1; i < st + cir.size(); i = j)
        {
            j = i + 1;
            while(j < st + cir.size() && edge[lin[j]].size() <= 1)  ++j;
            if(j - i < siz[lin[i]] - 1) {cout << 0; return 0;}  
            else if(j - i > siz[lin[i]] - 1)    f[idx] = f[idx] * 2ll % mod;
        }
        ans = ans * f[idx] % mod;
    }
    for(int i = 1; i <= n; ++i)
    {
        ll tot = mul[i];
        for(int j = 1; j + j <= num[i]; ++j)
        {   
            ll nw = c(num[i], j + j) * qzh[j + j] % mod * qmi(i, j) % mod;
            if(i > 1 && (i & 1))    nw = nw * qmi(2, num[i] - j - j) % mod;
            tot = add(tot, nw);
        }
        ans = ans * tot % mod;
    }
    cout << ans;
    return 0;
}

CF102978H Harsh Comments

每次操作完后选中的概率会变很不妙,转化为选中每个数的概率仍不变,但选中了已经删除的就不耗步数再选

这样每条评论被选的概率不变,其实是求最后一个选中的 \(A_i\) 前选了多少个 \(B_j\),即 \(A_i\) 之前最多有多少个 \(B\)

假设 \(x\) 被选的概率是 \(p\)\(y\) 被选的概率是 \(q\),则 \(x\)\(y\) 之前被选的概率是 \(\sum_{i=0}^{\infty}(1-p-q)^ip=\frac p{p+q}\)

用 min-max 容斥的思想,现在是求 \(E(\max_{i=1}^n A_i\text{前} B \text{的个数} )\),转化为 \(\sum_{S}(-1)^{|S|-1}E(\min_{i\in S} A_i\text{前} B\text{的个数})\)

那么会被统计的 \(B\) 就是必须在集合内所有 \(A\) 之前被选,而一个集合中选到任意一个的概率只和 \(\sum _{i\in S}A_i\) 有关,于是可以背包求出每种 \(\sum A_i\) 对应的 \(S\) 的容斥系数之和,同时每一个 \(B\) 是否在之前是独立的,可以直接分开统计答案

复杂度为 \(O(n\sum A)\)

code
int main()
{
    read(n, m);
    for(int i = 1; i <= n; ++i) read(a[i]), sum += a[i];
    for(int i = 1; i <= m; ++i) read(b[i]);
    f[0] = mod - 1;
    for(int i = 1; i <= n; ++i) // min-max 容斥,背包对 \sum_{a_i} = x 求出容斥系数 f[x]
        for(int j = sum; j >= a[i]; --j)    Add(f[j], mod - f[j - a[i]]);
    for(int i = 1; i <= m; ++i) // 期望线性,对每个 B_i 单独考虑
        for(int j = 1; j <= sum; ++j)   Add(ans, b[i] * qmi(add(j, b[i]), mod - 2) % mod * f[j] % mod);
    cout << add(ans, n);
    return 0;
}

图论

CF1458D Flip and Reverse

观察奇怪的操作,把 \(0\) 看成 \(-1\),发现若要把某个 \(1\) 变成 \(0\),那么翻转区间的末尾也是 \(1\),且这个操作相当于选择两个前缀和相同的位置,翻转中间的区间(不包括右端点)

如果把前缀和看成点,那每次可以选择走到后面的或前面的点,让字典序最小,即尽可能往前走

先建出原序列代表的图,那么我们要遍历图上的每条边,即走一条欧拉路径,翻转相当于是将一个环(欧拉回路)换方向遍历

什么时候能换方向呢?

如果本来在点 \(x\) 我们应该往后走,现在想往前,那么往前到的那个点除了这条边之外必须还有一条边可以回来

这是必要条件,但它也充分

前缀和序列原来形如 \(v, v+1,\dots,v,v-1,\dots,v\),后来变成 \(v,v-1,\dots,v,v+1,\dots,v\),只需要 \(v-1\)\(v\) 之间有 \(\ge 2\) 条边

code
void clear()  
{
    for(int i = 0; i <= n + n; ++i) sum[i][0] = sum[i][1] = 0;
}
void mian()
{
    cin >> (s + 1), n = strlen(s + 1), clear();
    for(int i = 1; i <= n; ++i) a[i] = s[i] - '0', qzh[i] = qzh[i - 1] + (a[i] ? 1 : -1);
    for(int i = 1; i <= n; ++i) ++sum[qzh[i - 1] + n][a[i]], ++sum[qzh[i] + n][a[i] ^ 1];
    int nw = 0;
    for(int i = 1; i <= n; ++i)
    {
        if(sum[nw + n][0] > 0 && (sum[nw + n][1] == 0 || sum[nw + n][0] > 1))   
            --sum[nw + n][0], putchar('0'), --nw, --sum[nw + n][1];
        else    --sum[nw + n][1], putchar('1'), ++nw, --sum[nw + n][0]; 
    }
    putchar('\n');
}

#670. 【UNR #5】获奖名单

看到这题就有想把字符看成点,长度为 \(2\) 的串两个字符之间有边,然后就不知道怎么办了

最后会形成回文串,先考虑总长度为偶数的情况,那么除了一一对应匹配的,剩下情况就是左侧有一个单个的,然后右侧对应位置是两个的,之后左右都是两个的,最后右边再有一个单个的结束交错

此时这样就对应着一条从单个字符出发,单个字符结束的路径,由于保证有解,因此走这样的尽量多的路径后,剩下的一定一一对应

比较妙的处理是建立虚点 \(s\),这样路径就是从 \(s\) 出发的欧拉回路,跑出欧拉回路即可构造方案,注意特判整个串中间可能是形如 \(AA\) 这样的串

总长度为奇数,就意味着中间的字符不用配对,找到那个字符,如果它是长度为 \(2\) 串的一段,那么找出它出发,到 \(s\) 结尾的一条欧拉路径即可

复杂度 \(O(n+m)\),细节比较多

code
void dfs(int x)
{
    for(int &i = st[x]; i < edge[x].size(); ++i)
    {
        auto [y, w] = edge[x][i];
        if(book[abs(w)])    continue;
        book[abs(w)] = 1, dfs(y);
        path[++cnt] = -w;
    }
}
void check()
{
    int a[N * 2] = {0}, tmp = 0;
    for(int x : zh)
        if(x < 0)   
            for(int i = len[-x] - 1; i >= 0; --i)   a[++tmp] = name[-x][i];
        else
            for(int i = 0; i < len[x]; ++i) a[++tmp] = name[x][i];
    for(int i = 1; i <= tmp - i + 1; ++i)
        if(a[i] != a[tmp - i + 1])  {cerr << i << " WA.\n";   exit(0);}
    cerr << "OK.\n";
}
int main()
{
    read(n, m);
    for(int i = 1; i <= n; ++i)
    {
        read(len[i]), tot += len[i];
        for(int j = 0; j < len[i]; ++j) read(name[i][j]);
        if(name[i][0] < name[i][1]) swap(name[i][0], name[i][1]), tag[i] ^= 1;
        if(num[{name[i][0], name[i][1]}] == 0)  num[{name[i][0], name[i][1]}] = ++idx;
        fr[i] = num[{name[i][0], name[i][1]}], has[fr[i]].pb(i);
        if(len[i] == 1) edge[m + 1].pb({name[i][0], i}), edge[name[i][0]].pb({m + 1, i});
        else    edge[name[i][0]].pb({name[i][1], i}), edge[name[i][1]].pb({name[i][0], -i});
    }
    dfs(m + 1);
    for(int i = 1; i <= n; ++i) 
        if(!book[i])    ++sum[fr[i]];
    if(tot % 2 == 0)
    {  
        for(int i = 1; i <= m; ++i)
            if(sum[num[{i, i}]] & 1)
            {
                for(int j = 1; j <= n; ++j)
                    if(!book[j] && name[j][0] == i && name[j][1] == i)  {zh.pb(j), --sum[fr[j]], book[j] = 1;   break;}
                break;
            }
    }
    for(int i = 1; i <= cnt; ++i)   (i & 1) ? rev.pb(path[i]) : zh.pb(-path[i]);
    for(int i = 1; i <= n; ++i)
    {
        if(book[i] || sum[fr[i]] == 0) continue;
        int cur = 0;
        for(int x : has[fr[i]])
            if(!book[x])    ((cur & 1) ? zh.pb(x) : rev.pb(-x)), book[x] = 1, cur ^= 1;
        sum[fr[i]] = 0;
    }
    reverse(zh.begin(), zh.end());
    for(int x : rev)    zh.pb(x);
    for(int x : zh) print(abs(x)), putchar(' ');
    putchar('\n');
    for(int x : zh) print(int(x < 0) ^ tag[abs(x)]), putchar(' ');
    return 0;
}

#605. 【UER #9】知识网络

发现种类数很少,意味着两点间最短路的长度最多是 \(2k\)(注意这里最短路定义为两点间最短路径的点数)

我们对于每种颜色进行统计,优化建图的方式是建立虚点,它向这种颜色的点连边权为 \(1\) 的有向边,这种颜色的点向它连边权为 $0 $ 的有向边

然后从虚点跑出到所有点的最短路,边权只有 \(0/1\) 可以使用 01 bfs

但是问题是不是所有点到达这种颜色的点都经过虚点,可能是它直接走最短路就能到目标节点,之后再通过虚点到其它点

\(x\) 到虚点的最短路上,可能经过的这种颜色的点,真实最短路距离是 \(x\) 到虚点最短路长度 \(-1\)

建出最短路的 DAG,统计每个点的前驱中这种颜色的点的个数,这只能暴力,用 bitset 优化

朴素的每次用长度为 \(n\)bitset 是不行的,因为每做一次复杂度都是 \(O(\frac{nm}w)\),这样总复杂度会是 \(O(k\frac{nm}w)\),就爆炸了

但所有颜色的点个数总和为 \(n\),每次只使用这种颜色点数量大小的 bitset,复杂度就是 \(O(\frac{nm}w)\)

使用变长 bitset 的技巧会带来空间问题,直接每 \(w\) 个数分一块做,时间复杂度不变,空间复杂度可以做到线性

code
void solve(int st, int nw)
{
    queue<int> q;
    for(int i = 1; i <= n + k; ++i) book[i] = 0, deg[i] = bac[i];
    ull sum = min(nw + 64, tot[st]) - nw;
    for(int i = nw; i < nw + 64 && i < tot[st]; ++i)   book[ord[st][i]] |= 1llu << (i - nw);
    for(int i = 1; i <= n + k; ++i)
        if(deg[i] == 0 && dis[i] <= (k << 1))    q.push(i);
    while(!q.empty())
    {
        int x = q.front();  q.pop();
        for(auto [y, w] : tu[x])
        {
            --deg[y], book[y] |= book[x];
            if(deg[y] == 0) q.push(y);
        }
    }
    for(int i = 1; i <= n; ++i)
        if(dis[i] <= (k << 1))   
        {
            ull tmp = __builtin_popcountll(book[i]);
            ans[dis[i]] += tmp, ans[dis[i] + 1] += 1llu * (sum - tmp);
        }
}
void work(int st)
{
    if(tot[st] == 0)    return;
    for(int i = 1; i <= n + k; ++i) dis[i] = (k << 1) + 5, tu[i].clear(), deg[i] = vis[i] = 0;
    deque<int> q;
    dis[n + st] = 0, q.pb(n + st);
    while(!q.empty())
    {
        int x = q.front();  q.pop_front();
        if(vis[x])  continue;
        vis[x] = 1;
        for(auto [y, w] : edge[x])
            if(dis[y] > dis[x] + w) 
            {
                dis[y] = dis[x] + w;
                w ? q.pb(y) : q.push_front(y);
            }
    }
    for(int x = 1; x <= n + k; ++x)
        for(auto [y, w] : edge[x])
            if(dis[y] == dis[x] + w)    tu[x].pb({y, w}), ++deg[y];   
    for(int i = 1; i <= n + k; ++i) bac[i] = deg[i];
    for(int i = 0; i < tot[st]; i += 64)   solve(st, i);
}
int main()
{
    read(n, m, k);
    for(int i = 1; i <= n; ++i) 
    {
        read(a[i]), ord[a[i]].pb(i), ++tot[a[i]];
        edge[a[i] + n].pb({i, 1}), edge[i].pb({a[i] + n, 0});
    }
    for(int i = 1; i <= m; ++i) read(u, v), edge[u].pb({v, 1}), edge[v].pb({u, 1});
    for(int i = 1; i <= k; ++i) work(i);
    ull tmp = 0;    ans[1] = 0;
    for(int i = 1; i <= k * 2; ++i) print(ans[i] >> 1), putchar(' '), tmp += ans[i] >> 1;
    print(1llu * n * (n - 1) / 2 - tmp);
    return 0;
}

CF1305G Kuroni and Antihype

假设有一个年龄为 \(0\) 的人邀请了所有自愿加入的人,那么整个邀请关系就形成了一棵树,每条边的权值是父节点的点权,想让总边权和最大,且每条边连接两个点点权与为 \(0\)

边权不好处理,但我们把每个点连向父亲的那条边的权值加上它的点权,那么边权形如 \(a_u+a_v\),且总和刚好多了 \(\sum_x a_x\)

剩下的事情就是找出这张图的最大生成树

可以直接跑 Kruskal,直接枚举子集,复杂度为 \(O(3^n)\),常数很小可以通过

但这种题考虑 Boruvka 算法,需要找出跟 \(x\) 不在一个连通块内,\(a_x\ \&\ a_y=0\) 最大的 \(a_y\)

于是考虑转化为求 \(x\ |\ y=x\) 最大的 \(y\),使用 FMT 维护最大值与和最大值不同连通块的最大值,就能完成转移

复杂度为 \(O((n+m2^m)\log n)\)

code
int find(int x) {return x != fa[x] ? fa[x] = find(fa[x]) : x;}
void merge(int x, int y)
{
    if(find(x) != find(y))  ans += 1ll * (a[x] + a[y]), fa[find(x)] = find(y);
}
int main()
{
    read(n);
    for(int i = 1; i <= n; ++i) read(a[i]), sum += 1ll * a[i];
    cnt = n + 1, S = (1 << 18) - 1, a[n + 2] = -V * 2;
    for(int i = 1; i <= n + 2; ++i) fa[i] = i;
    while(cnt > 1)
    {
        for(int i = 0; i <= S; ++i) f[i] = {n + 2, n + 2};
        for(int i = 1; i <= n + 1; ++i) f[a[i]] = {i, n + 2}, book[i] = 0, mx[i] = {n + 2, n + 2};
        auto chkmax = [&](pii x, pii y) -> pii
        {
            if(a[x.fi] < a[y.fi])   swap(x, y);
            if(find(y.fi) != find(x.fi))    x.se = a[y.fi] > a[x.se] ? y.fi : x.se;
            else if(find(y.se) != find(x.fi))   x.se = a[y.se] > a[x.se] ? y.se : x.se;
            return x;  
        };
        for(int j = 0; j <= 17; ++j)
            for(int i = 1; i <= S; ++i)
                if((i >> j) & 1)    f[i] = chkmax(f[i], f[i ^ (1 << j)]);
        for(int i = 1; i <= n + 1; ++i)
        {
            int s = S ^ a[i], x = find(i), y = n + 2;
            y = find(f[s].fi) != x ? f[s].fi : f[s].se;
            if(y < n + 2 && a[i] + a[y] > a[mx[x].fi] + a[mx[x].se])   mx[x] = {i, y};
        }
        for(int i = 1; i <= n + 1; ++i)
            if(mx[i].fi < n + 2 && mx[i].se < n + 2)  merge(mx[i].fi, mx[i].se);
        cnt = 0;
        for(int i = 1; i <= n + 1; ++i) 
        {
            if(!book[find(i)])  ++cnt;
            book[find(i)] = 1;
        }
    }
    cout << ans - sum;
    return 0;
}

[ARC103F] Distance Sums

考虑总和最大的一定是叶子,于是可以知道它父亲的权值,由于和两两不同,则直接能知道它的父亲是谁,并同时维护子树大小,从而处理父亲时也能知道它的父亲的和

找出重心然后往下填也可以,不过细节更多,要维护子树内剩余点数,我是这样写的

code
void dfs(int x)
{
    siz[x] = 1;
    for(int y : edge[x])
        dfs(y), f[x] += f[y] + siz[y], siz[x] += siz[y];
}
void Dfs(int x)
{
    for(int y : edge[x])
    {
        ll tmp = f[x] - (f[y] + siz[y]);
        f[y] += tmp + n - siz[y];
        Dfs(y);
    }
}
void check()
{
    for(int i = 1; i <= n; ++i) siz[i] = f[i] = 0;
    dfs(ord[1]), Dfs(ord[1]);
    for(int i = 1; i <= n; ++i)
        if(f[i] != dis[i])  {print(-1); exit(0);}
    for(int i = 1; i <= n; ++i)
        for(int j : edge[i])    print(i), putchar(' '), print(j), putchar('\n');
}
int main()
{
    read(n), op = (n & 1) ? 1 : 2;
    for(int i = 1; i <= n; ++i) read(dis[i]), mn = min(mn, dis[i]), ord[i] = i;
    if(n == 1)  {if(dis[1] != 0)    print(-1);  return 0;}
    sort(ord + 1, ord + n + 1, [&](const int x, const int y){return dis[x] < dis[y];});
    subt.insert({dis[ord[1]] + op, ord[1]});
    if(dis[ord[2]] == dis[ord[1]])
    {
        if(n & 1)   {print(-1); return 0;}
        st = 3, siz[ord[1]] = siz[ord[2]] = n / 2 - 1; 
        subt.insert({dis[ord[2]] + op, ord[2]});
    }
    else    siz[ord[1]] = n - 1;
    for(int i = st; i <= n; ++i)
    {
        int x = ord[i];
        iter it = subt.upper_bound({dis[x], N});
        if(it == subt.begin())  {print(-1); return 0;}
        --it;
        edge[it -> se].pb(x); 
        siz[x] = (n - (dis[x] - dis[it -> se])) / 2 - 1, siz[it -> se] -= siz[x] + 1;
        pll tmp = {dis[it -> se] + n - 2 * min(n / 2, siz[it -> se]), it -> se};
        subt.erase(*it);
        if(siz[tmp.se] > 0) subt.insert(tmp);
        if(siz[x] > 0)  subt.insert({dis[x] + n - 2 * siz[x], x});
    }
    check();
    return 0;
}

字符串

CF547E Mike and Friends

AC 自动机经典题,转化为 \(s_k\)\(s_{1\sim r}\) 中出现次数减去 \(s_{1\sim l}\) 中出现次数

那么 fail 树上 \(s_k\) 末尾节点的子树内节点所代表的串的后缀都是 \(s_k\),每次加入串 \(s_r\),把它含有的节点权值 \(+1\),那么查询 \(s_k\) 末尾节点的子树和即可

使用树状数组维护,复杂度 \(O(n\log n)\)

code
struct BIT
{
    int sum[N], n;
    void upd(int x, int val)    {for(; x <= n; x += x & (-x))   sum[x] += val;}
    int qry(int x)  {int res = 0;   for(; x; x -= x & (-x)) res += sum[x];  return res;}
    int ask(int l, int r)   {return qry(r) - qry(l - 1);}
}bit;
struct ACAM
{
    int ch[N][28], idx = 1, root = 1, sum[N], fail[N], cnt, dfn[N], mxdfn[N];
    vector<int> edge[N];
    void insert(int id)
    {
        int u = root, len = s[id].length();
        for(int i = 0; i < len; ++i)
        {
            if(!ch[u][s[id][i] - 'a'])  ch[u][s[id][i] - 'a'] = ++idx;
            u = ch[u][s[id][i] - 'a'];
        }
        ed[id] = u;
    }
    void dfs(int x) 
    {
        dfn[x] = ++cnt;
        for(int y : edge[x])    dfs(y);
        mxdfn[x] = cnt;
    }
    void upd(int id)  
    {
        int u = root, len = s[id].length();
        for(int i = 0; i < len; ++i)
        {
            u = ch[u][s[id][i] - 'a'];
            bit.upd(dfn[u], 1);
        }
    }
    void getfail()  
    {
        queue<int> q;
        for(int i = 0; i < 26; ++i)
            if(ch[root][i]) q.push(ch[root][i]), fail[ch[root][i]] = root;
            else    ch[root][i] = root;
        while(!q.empty())
        {
            int u = q.front();  q.pop();
            for(int i = 0; i < 26; ++i)
                if(ch[u][i])    fail[ch[u][i]] = ch[fail[u]][i], q.push(ch[u][i]);
                else    ch[u][i] = ch[fail[u]][i];
        }
        for(int i = root; i <= idx; ++i)    edge[fail[i]].pb(i);
    }
}ac;
int main()
{
    cin >> n >> q;
    for(int i = 1; i <= n; ++i) cin >> s[i], ac.insert(i);
    bit.n = ac.idx, ac.getfail(), ac.dfs(ac.root);
    for(int i = 1; i <= q; ++i)
    {
        cin >> l >> r >> k;
        ask[l - 1].pb({k, -i}), ask[r].pb({k, i});
    }
    for(int i = 1; i <= n; ++i)
    {
        ac.upd(i);
        for(pii x : ask[i])
            if(x.se > 0)    ans[x.se] += bit.ask(ac.dfn[ed[x.fi]], ac.mxdfn[ed[x.fi]]);
            else    ans[-x.se] -= bit.ask(ac.dfn[ed[x.fi]], ac.mxdfn[ed[x.fi]]);
    }
    for(int i = 1; i <= q; ++i) print(ans[i]), putchar('\n');
    return 0;
}

CF914F Substrings in a String

在中间修改字符显然不好做,只能考虑比较暴力的做法

匹配的过程是一个一个字符的比较,加速这个过程,我们维护当前左端点代表的串还未失配的集合,每次匹配一个字符,找到这个字符存在的位置,并取两个集合的交集

使用 bitset 优化,复杂度 \(O(\frac{nq}w)\)

code
int main()
{
    cin >> (s + 1) >> q, n = strlen(s + 1);
    for(int i = 1; i <= n; ++i) has[s[i] - 'a'][i] = 1;
    for(int i = 1; i <= q; ++i)
    {
        cin >> op >> l;
        if(op == 1) 
        {
            cin >> c;
            has[s[l] - 'a'][l] = 0, has[c - 'a'][l] = 1, s[l] = c;
        }
        else
        {
            cin >> r >> (t + 1), m = strlen(t + 1);
            f.set(), f = (f >> l) << l, f = (f << (N - 1 - (r - m + 1))) >> (N - 1 - (r - m + 1));
            for(int j = 1; j <= m; ++j) f &= has[t[j] - 'a'] >> (j - 1);
            print(f.count()), putchar('\n');
        }
    }
    return 0;
}

数学

CF1553H XOR and Distance

要对每个 \(x\) 求出 \(\min_{i\ne j}|(a_i\oplus x)-(a_j \oplus x)|\),关于异或的信息,想到按位考虑

\(f_{i,j}\) 表示当考虑了前 \(i\) 位且 \(a\) 只有前 \(i\) 位能与 \(x\) 不同时 \(x=j\) 的答案,$ mx_{i,j},mn_{i,j}$ 为此时最大,最小的 \(a\)

转移则考虑新增一位不相同的,在 $x $ 与 \(x \oplus 2^i\) 之间转移,\(mn,mx\) 可以直接更新,考虑 \(f_x\),如果 \(a\) 的第 \(i\) 位不同,则 \(a \oplus x\) 大的那边取最小的,小的那边取最大的,\(mn_{j\oplus 2^i}-mx_{j\oplus 2^i}\),如果相同,则相当于同加同减,差不变,直接用 \(f_{x\oplus 2^i}\) 更新 \(f_x\)

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

为啥放到数学板块呢,因为这就是 FWT 迭代 DFT 的过程

其实这个题也可以用 Trie 维护,同样维护当前节点的答案,子树内数减去它代表区间的最小值后的最小值和最大值,异或一个数,相当于对这一位为 \(1\) 的位交换左右子树,它子树内的和祖先的信息其实都没变,只需改变这一层

假设交换从低到高第 \(i\) 位,则复杂度为 \(2^{k-i-1}\),但是如果将 \(x\)\(0\) 开始逐渐增大,那么第 \(i\) 位会变化 \(2^{k-i}\) 次,复杂度依然不对,有个技巧是我们假装枚举的数是翻转的,即第 \(i\) 位是第 \(k-i-1\) 位,那么复杂度就是 \(O(k2^k)\)

code
int main()
{
    read(n, k);
    for(int i = 0; i < (1 << k); ++i)   mn[i] = f[i] = N, mx[i] = -N; 
    for(int i = 1; i <= n; ++i) read(a[i]), mn[a[i]] = mx[a[i]] = 0;
    for(int i = 0; i < k; ++i) // 按位加入,每次只考虑只有前 i 位和 x 不同的数,跟某模拟赛 T1 好像,本质上都是 FWT ??? bzd 啊 
    {
        for(int x = 0; x < (1 << k); ++x)   tmn[x] = mn[x], tmx[x] = mx[x], g[x] = f[x];
        for(int x = 0; x < (1 << k); ++x)
        {
            int y = x ^ (1 << i);
            f[x] = min({f[x], tmn[y] + (1 << i) - tmx[x], g[y]});
            mn[x] = min(mn[x], tmn[y] + (1 << i)), mx[x] = max(mx[x], tmx[y] + (1 << i));
        }
    }
    for(int i = 0; i < (1 << k); ++i)   print(f[i]), putchar(' ');
    return 0;
}

P5339 [TJOI2019] 唱、跳、rap和篮球

容斥原理,枚举钦定有 \(i\) 组不符合,关键是求剩下数的摆放方案数

式子形如

\[\sum_{i=1}^a {n-4x\choose i}\sum_{j=1}^b {n-4x-i\choose j}\sum_{k=1}^c{n-4x-i-j\choose k}[n-4x-i-j-k\le d] \]

可以用前缀和优化至单次 \(O(n^2)\),但需要单次 \(O(n)\)

先枚举 \(a,b\) 中选的数的总和 \(y\),则

\[\sum_{y=1}^{a+b}{n-4x\choose y}\sum_{i=\max\{1,y-b\}}^a{y\choose i}\sum_{j=1}^c{n-4x-y\choose j}[n-4x-y-j\le d] \]

发现枚举 \(y\),后面的就可以用组合数前缀和算出来,注意边界即可

code
ll calc(ll n)
{
	ll res = 1, tot = 0;
	for(ll i = max(n - a[3] - a[4], 0ll); i <= a[1] + a[2] && i <= n; ++i)
	{
		ll l1 = max(0ll, i - a[2]), r1 = min(a[1], i);
		ll l2 = max(0ll, n - i - a[4]), r2 = min(a[3], n - i);
		if(l1 > r1 || l2 > r2)	continue;
		res = C[n][i] * add(qzh[i][r1], l1 ? mod - qzh[i][l1 - 1] : 0) % mod;
		res = res * add(qzh[n - i][r2], l2 ? mod - qzh[n - i][l2 - 1] : 0) % mod;
		tot = add(tot, res);
	}
	return tot;
}
int main()
{
        cin >> n >> a[1] >> a[2] >> a[3] >> a[4];
        for(int j = 1; j <= 4; ++j) a[j] = min(a[j], n);
	C[0][0] = qzh[0][0] = 1;
	for(ll i = 1; i <= n; ++i)
	{
		C[i][0] = qzh[i][0] = 1;
		for(ll j = 1; j <= i; ++j)	C[i][j] = add(C[i - 1][j - 1], C[i - 1][j]); 
		for(ll j = 1; j <= i; ++j)	qzh[i][j] = add(qzh[i][j - 1], C[i][j]);
	}
	ans = calc(n);
	cerr << ans << "\n";
	for(ll i = 1; i <= n / 4; ++i)
	{
		ll flag = 1;
		for(ll j = 1; j <= 4; ++j)
			if(a[j] <= 0)	{flag = 0;	break;}
		if(!flag)	break;
		for(ll j = 1; j <= 4; ++j)  --a[j];
		ll tot = calc(n - i * 4) * C[n - i * 3][i] % mod;
		if(i & 1)	ans = add(ans, mod - tot);
		else	ans = add(ans, tot);
		// cerr << tot << "\n";
	}
	printf("%lld", ans);
	return 0;
}

CF908D New Year and Arbitrary Arrangement

想设 \(f(i,j)\) 表示已经有 \(i\) 个子序列且有 \(j\)\(a\) 时的概率,但是可能一直随机到 \(a\) 而没有 \(b\) 导致 \(j\) 这维可能无限大

所以当 \(i+j\ge k\) 时表示再有一个 \(b\) 就能停止,直接求出此时的答案,\(\sum_{x=0}^{\infty}pr_a^xpr_b(i+j+x)\),拆开括号用等比数量求和即可计算

剩下的则是 DP 求出,复杂度 \(O(k^2)\)

code
int main()
{
    cin >> k >> pra >> prb;
    f[0][1] = 1;
    ll lsh = qmi(pra + prb, mod - 2);
    pra = pra * lsh % mod, prb = prb * lsh % mod;
    for(int i = 0; i < k; ++i)
    {
        for(int j = 1; j + i < k; ++j)
        {
            Add(f[i][j + 1], f[i][j] * pra % mod);
            Add(f[i + j][j], f[i][j] * prb % mod);
        }
    }
    for(int i = 0; i <= k + k; ++i)
        for(int j = 1; j <= k; ++j)
        {
            if(i >= k)  Add(ans, 1ll * f[i][j] * i % mod);
            else if(i + j >= k) 
            {
                Add(ans, f[i][j] * prb % mod * qmi(add(1, mod - pra), mod - 2) % mod * (i + j) % mod);
                Add(ans, f[i][j] * prb % mod * pra % mod * qmi(add(1, mod - pra) * add(1, mod - pra) % mod, mod - 2) % mod);
            }
        }
    cout << ans;
    return 0;
}

CF103415J Cafeteria

之前模拟赛搬过加强版本,这里就是用转置原理快速对系数矩阵求逆,把每个位置的值看成对一个向量的线性变换,乘法就是变换过去,求逆后相当于逆变换

code
int main()
{
    cin >> n >> m >> q >> (s + 1) >> (t + 1);
    for(int j = 0; j <= m; ++j) f[j][j] = g[j][j] = 1;
    inv[0][0] = 1;
    for(int i = 1; i <= n; ++i)
    {   
        for(int j = m; j > 0; --j)
        {
            if(t[j] != s[i])    continue;
            for(int k = 0; k <= m; ++k) Add(f[k][j], f[k][j - 1]);
            for(int k = 0; k <= m; ++k) Add(g[k][j - 1], mod - g[k][j]);
        }
        for(int j = 0; j <= m; ++j) mul[i][j] = f[j][m], inv[i][j] = g[j][0];
    }
    for(int i = 1; i <= q; ++i)
    {
        cin >> l >> r;
        ll sum = 0;
        for(int j = 0; j <= m; ++j) Add(sum, mul[r][j] * inv[l - 1][j] % mod);
        ans ^= sum;
    }
    cout << ans;
    return 0;
}
posted @ 2024-04-11 17:54  KellyWLJ  阅读(17)  评论(0编辑  收藏  举报