MX 暑假集训 7.10

【初音ミク】蜘蛛絲Monopoly【sasakure.UK】

神秘 dp 来袭。

T1: Awkward

题意

给定一棵 \(n\) 个节点的树,你需要求有多少个排列满足对于相邻两项都不为树上的一条边,答案对 \(10^9+7\) 取模。

\(2\le n\le 5\times 10^3\)

solution

只会链的情况喵,不会排列计数喵。

考虑容斥,钦定一个边集 \(S \subseteq E\) 一定满足这些边在排列中都相邻,其他的边随意,令方案数为 \(F(S)\),则答案为 \(\sum (-1)^{\lvert S \rvert }F(S)\)

注意到对于一个边集 \(S\) 一定不会使得一个点的度数大于 \(2\),因为若一个点度数大于 \(2\) 则会使得它需要相邻的点超过两个,不符合排列的需求。

这样的话一个边集就可以看作若干条链,而每条长度不为 \(1\) 的链上的方案数只有两个(正着放和反着放),将每个连通块缩成一个点,这些点之间可以任意排列,所以对于一个边集 \(S\),令 \(c(S)\) 为其形成长度不为 \(1\) 的链的数量,\(\lvert S \rvert =k\),则有 \(F(S)=2^{c(S)}(n-k)!\)

那么令 \(A_k=\sum\limits_{\lvert S\rvert =k}2^{c(S)}\),答案就为 \(\sum\limits_{k=0}^{n-1} (-1)^{k}A_k(n-k)!\)

考虑用个树形 dp 求出 \(A_k\),令 \(f_{i,j,k}\) 表示在第 \(i\) 个节点子树及其连向父亲的边中共选了 \(j\) 条,连向父亲的边是否被选上的每种方案权值之和。

我们将 \(2\) 倍的贡献统一在链顶加上。

我们先将儿子合并起来,令 \(g_{i,t}\) 表示考虑完若干子节点,其中共选了 \(i\) 条边,有 \(t\) 个子节点连向当前节点的所有方案权值和,这里的 \(i\) 条边不包括当前节点与父亲的连边,同时 \(0\le t\le 2\),因为这个点连边数量不超过 \(2\)

对每个子节点 \(v\)\(g\) 的转移是:

\[g_{i+j,0}\leftarrow g_{i,0}\times f_{v,j,0} \]

\[g_{i+j,1}\leftarrow g_{i,1}\times f_{v,j,0} + g_{i,0}\times f_{v,j,1} \]

\[g_{i+j,2}\leftarrow g_{i,2}\times f_{v,j,0} + g_{i,1}\times f_{v,j,1} \]

处理完所有子节点后,\(g\)\(f\) 的转移是:

\[f_{u,i,0}\leftarrow g_{i,0}+g_{i,1}\times 2+g_{i,2}\times 2 \]

\[f_{u,i,1}\leftarrow g_{i-1,0}+g_{i-1,1} \]

这样就做完了,由树形背包的时间复杂度,两个节点仅在 lca 处贡献一次,所以时间复杂度是 \(O(n^2)\) 的。

Code
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=5e3+10;
constexpr int inf=1e9+10;

constexpr int mod=1e9+7;
inline int ad(int x,int y) {return ((x+y>=mod)?(x+y-mod):(x+y));}
inline void add(int &x,int y) {x=ad(x,y);}

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n;
int siz[N],f[N][N][2],g[N][N][3],tmp[N][3];
ll fac[N];
vector<int> e[N];

inline void dfs(int now) {
	siz[now]=1;
	g[now][0][0]=1;
	for (int v:e[now]) {
		dfs(v);
		f(i,0,siz[now]) f(k,0,2) tmp[i][k]=g[now][i][k],g[now][i][k]=0;
		f(i,0,siz[now]) f(j,0,siz[v]) {
			add(g[now][i+j][0],1ll*tmp[i][0]*f[v][j][0]%mod);
			add(g[now][i+j][1],(1ll*tmp[i][0]*f[v][j][1]+1ll*tmp[i][1]*f[v][j][0])%mod);
			add(g[now][i+j][2],(1ll*tmp[i][1]*f[v][j][1]+1ll*tmp[i][2]*f[v][j][0])%mod);
		}
		siz[now]+=siz[v];
	}
	f(i,0,siz[now]) {
		f[now][i][0]=(g[now][i][0]+2ll*g[now][i][1]+2ll*g[now][i][2])%mod;
		if (i) f[now][i][1]=ad(g[now][i-1][0],g[now][i-1][1]);
	}
}

int main() {
	freopen("permutation.in","r",stdin);
	freopen("permutation.out","w",stdout);
	read(n);fac[0]=1;
	f(i,1,n) fac[i]=fac[i-1]*i%mod;
	f(i,2,n) {
		int fa;read(fa);
		e[fa].push_back(i);
	}
	dfs(1);
	ll ans=0;
	f(i,0,n-1) {
		int op=((i&1)?-1:1);
		ans+=op*f[1][i][0]*fac[n-i]%mod;
	}
	printf("%lld\n",(ans%mod+mod)%mod);
	return 0;
}

T2: 字典树收益

题意

给定长度为 \(n\) 仅由 \(0,1,?\) 组成的字符串,你需要将其中的 \(?\) 填成 \(0\)\(1\),然后再找出一个子序列,将这个子序列提取出来得到一个新字符串。

你需要将这个字符串分成若干个子串 \(s_1,s_2,s_3,\dots ,s_t\) 满足字典序单调递增,收益为将它们插入字典树后的节点数量,代价为 \(\sum\limits_{i=1}^{t}cost_{\lvert s_i\rvert }\),其中 \(cost\) 是给定的代价数组。

求出收益与代价之差的最大值。

\(1\le T\le 100,1\le n\le 200,1\le \sum n^3\le 5\times 10^7,0\le cost_i\le i\)

solution

很好的 dp,但是有点难。

首先有一个暴力的 dp:\(dp_{i,j}\) 表示 \([1,i]\) 中最后一个字符串为 \(j\) 的最大收益,但是发现 \(j\) 很大,复杂度不对。

注意到答案一定在 \([0,n]\) 内,并且 dp 时的值域也是 \([0,n]\)

那么我们考虑换一下维度,\(g_{i,j}\) 表示 \([1,i]\) 中答案为 \(j\),最后一个字符串字典序的最小值。

根据贪心,最后一个字符串字典序显然是越小越好,因为这样不仅可以接更多的字符串,而且得到的贡献显然更大。

那么如何转移呢?我们需要重新开启一个段,令原来的字符串为 \(s\),新的字符串为 \(t\),那么合法的转移就是 \(t\) 字典序小于 \(s\),贡献为 \(t\) 的长度减去 \(t\)\(s\) 的 LCP 长度再减去 \(t\) 的长度对应代价。

用一个辅助数组 \(f_{i,j,k}\) 辅助转移,表示考虑到第 \(i\) 个位置,除去最后一段长度所带来的代价后的总贡献为 \(j\),最后一段长度字典序已经大于原串,且最后一段未闭合,其长度为 \(k\) 的最小字典序。

转移可以分成三个部分:

  • \(f\) 闭合,对 \(g\) 进行转移;
  • \(f\) 后加入字符,对 \(f\) 转移;
  • \(g\) 后面新加入一个字符串,对 \(f\) 转移。

\(f\)\(g\) 的转移和 \(f\)\(f\) 的转移是平凡的,直接计入贡献即可。

着重讲一下 \(g\)\(f\) 的转移。

枚举一下 LCP,考虑最近的位置 \(x\) 使得 \([i+1,x]\) 中存在一个子序列等于 LCP,这个可以用指针维护或者预处理出来。

然后 LCP 的下一个位置需要满足原串为 \(1\),新串为 \(0\),或者原串后面为空,新串不为空,找到下一个为 \(0\) 或为 \(1\) 的位置可以预处理。

然后进行转移,只需要满足刚好比原串字典序大即可,后面的字符由 \(f\)\(f\) 的转移中逐步加上。

用 bitset 维护 01 串的字典序,取较小值可以用 bitset 异或后 _Find_first 得到第一个不同位置,前两个转移时间复杂度是 \(O(\frac{n^4}{\omega})\) 的,\(g\)\(f\) 的转移需要枚举 LCP,所以时间复杂度也是 \(O(\frac{n^4}{\omega})\) 的。

总时空复杂度 \(O(\frac{n^4}{\omega})\)

这个题给我们的 dp 带来了一个小巧思:当值域比较小且状态比较大时,不妨考虑换一下状态和值域的位置。

Code
#include<cstdio>
#include<algorithm>
#include<bitset>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=200+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n;
char S[N];
int c[N],nx1[N],nx0[N];

struct Node {
	int len;bitset<N> bit;
	inline void set() {len=n+1;bit.set();bit.set(0,0);}
	inline void reset() {len=0;bit.reset();}
	inline void push_back(int x) {len++;bit.set(len,x);}
	inline void pop_back() {len--;}
	inline bool operator [](const int x)const {return bit[x];}
};
Node f[N][N][N],g[N][N];

inline bool chk1(Node &x) {return x.len==n+1;}
inline void chkmin(Node &x,Node &y) {
	bitset<N> r=(x.bit^y.bit);
	int now=r._Find_first();
	if (now<=min(x.len,y.len)) {if (x.bit[now]>y.bit[now]) x=y;}
	else if (y.len<x.len) x=y;
}

inline void out(bitset<N> tmp) {
	f(i,1,n) printf("%d",tmp.test(i));
	putchar(10);
}

inline void solve() {
	read(n);
	scanf("%s",S+1);
	f(i,1,n) read(c[i]);
	f(i,0,n) f(j,0,n) f(k,0,n) f[i][j][k].set();
	f(i,0,n) f(j,0,n) g[i][j].set();
	f(i,0,n) g[i][0].reset();
	nx1[n+1]=nx0[n+1]=0;
	F(i,n,1) nx1[i]=((S[i]=='1'||S[i]=='?')?i:nx1[i+1]);
	f[0][0][0].reset();
	f(i,0,n) {
		f(j,0,n) f(k,0,n) if (!chk1(f[i][j][k])) {
			Node nxt=f[i][j][k];
			int val=j-c[k];
			if (val>=0) chkmin(g[i][val],nxt);
			chkmin(f[i+1][j][k],nxt);
			nxt.push_back((S[i+1]=='?')?0:(S[i+1]-'0'));
			chkmin(f[i+1][j+1][k+1],nxt);
		}
		f(j,0,n) if (!chk1(g[i][j])) {
			int now=i,len=g[i][j].len;
			Node lcp;
			lcp.reset();
			f(k,1,len) {
				if (k!=1) {
					now++;while (now<=n&&S[now]!='?'&&(S[now]-'0')!=g[i][j][k-1]) now++;
					lcp.push_back(g[i][j][k-1]);
				}
				if (now>n) break;
				if (g[i][j][k]) continue;
				int pos=nx1[now+1];
				if (!pos) continue;
				lcp.push_back(1);
				chkmin(f[pos][j+1][k],lcp);
				lcp.pop_back();
			}
			now++;while (now<=n&&S[now]!='?'&&(S[now]-'0')!=g[i][j][len]) now++;
			lcp.push_back(g[i][j][len]);
			if (now>n) continue;
			int pos=nx0[now+1];
			if (pos) {
				lcp.push_back(0);
				chkmin(f[pos][j+1][len+1],lcp);
				lcp.pop_back();
			}
			pos=nx1[now+1];
			if (pos) {
				lcp.push_back(1);
				chkmin(f[pos][j+1][len+1],lcp);
				lcp.pop_back();
			}
		}
	}
	F(j,n,0) if (!chk1(g[n][j])) return printf("%d\n",j),void();
}

int main() {
	freopen("trie.in","r",stdin);
	freopen("trie.out","w",stdout);
	int T;
	read(T);
	while (T--) solve();
	return 0;
}

T3: 路径染色

题意

给定一棵 \(n\) 个节点的树,每个节点 \(i\) 有一个人。

你需要给每个人确定一个终点,然后按照你指定的顺序依次走向终点,第 \(i\) 个人会将经过的边都染成 \(c_i\) 的颜色(覆盖原本颜色),按顺序给出所有边,求每条边都被染色时按边的顺序得到边的颜色序列字典序最小值。

\(1\le n\le 10^5,1\le T\le 5\)

solution

Hyperregular Bracket Strings

题意

给定 \(k\)\([l_i,r_i]\subseteq [1,n]\),求出长度为 \(n\) 的括号序列满足对于所有子段 \([l_i,r_i]\) 都是合法括号序列的方案数,答案对 \(998244353\) 取模。

\(1\le n,k\le 3\times 10^5\)

solution

这不是我们的 [ICPC 2015 WF] Tours 弱化版吗,五分钟秒了。

两个限制区间的交也必然是合法括号序列,稍微推一下可以看出随便选出若干限制区间进行各种交并补运算得到区间都必然是合法括号序列,也就是每个位置有覆盖它的限制区间的集合,将这个集合相同所有位置拿出来,它们必须是一个合法括号序列,且这个条件是充要的。

对每个限制区间异或上随机权值,做个异或哈希,最后得到的各个序列用卡特兰数乘起来即可。

可以做到 \(O(n+k)\)

Code
#include<cstdio>
#include<algorithm>
#include<random>
#include<unordered_map>
using namespace std;
#define ll long long
#define ull unsigned long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=3e5+10;
constexpr int inf=1e9+10;
constexpr int mod=998244353;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

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

int n,m;
ll C[N];
ull H[N];

mt19937 Ff(472130);
inline ull rnd() {
	ull k1=Ff(),k2=Ff();
	return (k1<<32)|k2;
}

inline void solve() {
	read(n);read(m);
	f(i,1,m) {
		int l,r;read(l);read(r);
		ull k=rnd();H[l]^=k;H[r+1]^=k;
	}
	unordered_map<ull,int> mp;
	f(i,1,n) {
		H[i]^=H[i-1];
		mp[H[i]]++;
	}
	f(i,1,n+1) H[i]=0;
	ll ans=1;
	for (pair<ull,int> p:mp) {
		if (p.second&1) ans=0;
		else ans=ans*C[p.second>>1]%mod;
	}
	printf("%lld\n",ans);
}

int main() {
	C[0]=1;
	f(i,1,300000) C[i]=C[i-1]*(i*4-2)%mod*qpow(i+1,mod-2)%mod;
	int T;
	read(T);
	while (T--) solve();
	return 0;
}

Subset with Zero Sum

题意

给定一个长度为 \(n\) 的序列 \(a\) 满足 \(i-n\le a_i\le i-1\),找出一个子序列满足 \(a_i\) 之和为 \(0\),可以证明必然存在这样的子序列。

\(1\le n\le 10^6\)

solution

神秘构造题。

我们发现这个 \(a_i\) 的值域非常的神秘,于是我们可以换一下位置,令 \(b_i=i-a_i\),则 \(1\le b_i\le n\)

\(\sum a_i=\sum i-(i-a_i)=\sum i-\sum b_i\),于是我们需要找到子序列满足 \(\sum b_i=\sum i\) 即可。

每个 \(i\)\(b_i\) 连边,最后形成一个基环森林,找到环即为答案。

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

Code
#include<cstdio>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=1e6+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;int f=1;
	char ch=getchar();
	while (ch<48) {if(ch=='-')f=-1;ch=getchar();} 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();x*=f;
}

int n;
int d[N];
vector<int> e[N],ans;

inline void topu() {
	queue<int> q;
	f(i,1,n) if (!d[i]) q.push(i);
	while (!q.empty()) {
		int now=q.front();
		q.pop();
		for (int v:e[now]) {
			d[v]--;
			if (!d[v]) q.push(v);
		}
	}
	f(i,1,n) if (d[i]) ans.push_back(i);
}

inline void solve() {
	read(n);
	f(i,1,n) {
		int x;read(x);
		e[i].push_back(i-x);
		d[i-x]++;
	}
	topu();
	printf("%d\n",(int)(ans.size()));
	for (int x:ans) printf("%d ",x);
	putchar(10);
	f(i,1,n) d[i]=0,e[i].clear();
	ans.clear();
}

int main() {
	int T;
	read(T);
	while (T--) solve();
	return 0;
}
posted @ 2026-07-10 17:32  Ff472130  阅读(14)  评论(0)    收藏  举报