bzoj4134 ljw和lzr的hack比赛 trie树合并

题目分析

首先,我们删掉所有被Hack的点,剩下的点的父亲,为原树上它的第一个没有被Hack的祖先。则产生了一个森林。

那么对于这个游戏局面,sg值为每棵树的sg值的异或和。

现在考虑一棵树的sg怎么算。记 g ( x ) g(x) g(x)为在这棵树中选一个节点 x x x,将 x x x与其祖先全部删除,剩下的子树们的sg异或和。那么该树中所有 g ( x ) g(x) g(x)的mex就是该树的sg。

我们发现,假设我们现在在处理子树 x x x,处理子树 y y y y y y x x x的父亲)的时候,子树 x x x中所有节点的 g ( x ) g(x) g(x)都要异或 y y y除了 x x x以外的儿子们的sg值。于是考虑用trie树来维护 g ( x ) g(x) g(x),异或就只要打标记就可以了。

然后做trie树合并获得 y y y的trie树,我们知道trie树合并的复杂度是 O ( n log ⁡ n ) O(n\log n) O(nlogn)的。

再插入所有 y y y的所有儿子异或和,即 g ( y ) g(y) g(y)

求出整棵trie树的mex,方法是记录trie树中以每个节点为根的子树是否是满的,如果是当前节点的左子树是满的,则在右子树中寻找这个mex值,否则在左子树中找。(往左走的边是0,往右走的边是1)

为了获得答案,我们还要记录每个 g ( x ) g(x) g(x)值对应的 x x x,这个只要对于trie树上的每个叶子节点开一个链表即可。

代码

#include<bits/stdc++.h>
using namespace std;
#define RI register int
int read() {
	int q=0;char ch=' ';
	while(ch<'0'||ch>'9') ch=getchar();
	while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
	return q;
}
const int N=100005,mxd=25;
int c[N],h[N],ne[N<<1],to[N<<1],fa[N],sg[N],st[N];
int bin[35],s[N*25][2],full[N*25],rt[N],tag[N*25],fir[N*25],nxt[N],tail[N*25];
int n,tot,ans,SZ,top,debug;

void puttag(int x,int num,int d) {
	if(num&bin[d]) swap(s[x][0],s[x][1]);
	tag[x]^=num;
}
void pd(int x,int d) {
	if(s[x][0]) puttag(s[x][0],tag[x],d-1);
	if(s[x][1]) puttag(s[x][1],tag[x],d-1);
	tag[x]=0;
}
void up(int x) {full[x]=full[s[x][0]]&full[s[x][1]];}
int merge(int x,int y,int d) {
	if(!x||!y) return x|y;
	if(d<0) {
		nxt[tail[x]]=fir[y],tail[x]=tail[y];
		return x;
	}
	if(tag[x]) pd(x,d); if(tag[y]) pd(y,d);
	s[x][0]=merge(s[x][0],s[y][0],d-1);
	s[x][1]=merge(s[x][1],s[y][1],d-1);
	up(x);return x;
}
void ins(int &x,int d,int num,int id) {
	if(!x) x=++SZ;
	if(d<0) {
		full[x]=1;
		if(!fir[x]) fir[x]=tail[x]=id;
		else nxt[tail[x]]=id,tail[x]=id;
		return;
	}
	if(tag[x]) pd(x,d);
	if(num&bin[d]) ins(s[x][1],d-1,num,id);
	else ins(s[x][0],d-1,num,id);
	up(x);
}
int mex(int x,int d) {
	if(d<0) return 0;
	if(tag[x]) pd(x,d);
	if(!full[s[x][0]]) return mex(s[x][0],d-1);
	else return bin[d]+mex(s[x][1],d-1);
}
void work(int x) {
	int sum=0;
	for(RI i=h[x];i;i=ne[i]) work(to[i]),sum^=sg[to[i]];
	for(RI i=h[x];i;i=ne[i]) puttag(rt[to[i]],sum^sg[to[i]],mxd);
	for(RI i=h[x];i;i=ne[i]) rt[x]=merge(rt[x],rt[to[i]],mxd);
	ins(rt[x],mxd,sum,x),sg[x]=mex(rt[x],mxd);
}
void getans(int x,int d,int num) {
	if(d<0) {
		for(RI i=fir[x];i;i=nxt[i]) st[++top]=i;
		return;
	}
	if(tag[x]) pd(x,d);
	if(num&bin[d]) getans(s[x][1],d-1,num);
	else getans(s[x][0],d-1,num);
}

void add(int x,int y) {to[++tot]=y,ne[tot]=h[x],h[x]=tot;}
void dfs(int x,int las,int OvO) {
	fa[x]=OvO;
	for(RI i=h[x];i;i=ne[i]) {
		if(to[i]==las) continue;
		if(!c[x]) dfs(to[i],x,x);
		else dfs(to[i],x,OvO);
	}
}
int main()
{
	int x,y;
	n=read();
	bin[0]=1;for(RI i=1;i<=mxd;++i) bin[i]=bin[i-1]<<1;
	for(RI i=1;i<=n;++i) c[i]=read();
	for(RI i=1;i<n;++i) x=read(),y=read(),add(x,y),add(y,x);
	dfs(1,0,0);
	tot=0;for(RI i=1;i<=n;++i) h[i]=0;
	for(RI i=1;i<=n;++i) if(!c[i]&&fa[i]) add(fa[i],i);
	for(RI i=1;i<=n;++i) if(!c[i]&&!fa[i]) work(i),ans^=sg[i];
	if(!ans) puts("-1");
	else {
		for(RI i=1;i<=n;++i)
			if(!c[i]&&!fa[i]) getans(rt[i],mxd,ans^sg[i]);
		sort(st+1,st+1+top);
		for(RI i=1;i<=top;++i) printf("%d\n",st[i]);
	}
	return 0;
}
题目描述 有一个 $n$ 个点的棋盘,每个点上有一个数字 $a_i$,你需要从 $(1,1)$ 走到 $(n,n)$,每次只能往右或往下走,每个格子只能经过一次,路径上的数字为 $S$。定义一个点 $(x,y)$ 的权值为 $a_x+a_y$,求所有满足条件的路径中,所有点的权值的最小值。 输入格式 第一行一个整数 $n$。 接下来 $n$ 行,每行 $n$ 个整数,表示棋盘上每个点的数字。 输出格式 输出一个整数,表示所有满足条件的路径中,所有点的权值的最小值。 数据范围 $1\leq n\leq 300$ 输入样例 3 1 2 3 4 5 6 7 8 9 输出样例 25 算法1 (形dp) $O(n^3)$ 我们可以先将所有点的权值求出来,然后将其看作是一个有权值的图,问题就转化为了在这个图中求从 $(1,1)$ 到 $(n,n)$ 的所有路径中,所有点的权值的最小值。 我们可以使用形dp来解决这个问题,具体来说,我们可以将这个图看作是一棵,每个点的父节点是它的前驱或者后继,然后我们从根节点开始,依次向下遍历,对于每个节点,我们可以考虑它的两个儿子,如果它的两个儿子都被遍历过了,那么我们就可以计算出从它的左儿子到它的右儿子的路径中,所有点的权值的最小值,然后再将这个值加上当前节点的权值,就可以得到从根节点到当前节点的路径中,所有点的权值的最小值。 时间复杂度 形dp的时间复杂度是 $O(n^3)$。 C++ 代码 算法2 (动态规划) $O(n^3)$ 我们可以使用动态规划来解决这个问题,具体来说,我们可以定义 $f(i,j,s)$ 表示从 $(1,1)$ 到 $(i,j)$ 的所有路径中,所有点的权值为 $s$ 的最小值,那么我们就可以得到如下的状态转移方程: $$ f(i,j,s)=\min\{f(i-1,j,s-a_{i,j}),f(i,j-1,s-a_{i,j})\} $$ 其中 $a_{i,j}$ 表示点 $(i,j)$ 的权值。 时间复杂度 动态规划的时间复杂度是 $O(n^3)$。 C++ 代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值