UOJ NOI Round 4

科技不够场。

序列妙妙值

出题人 01 很喜欢加法,也很喜欢异或运算(即 C/C++ 里的 ^ 运算符)。有天他一拍脑袋:把两个运算混一起,岂不是妙极了?

对于一个序列 \(b_1, \dots, b_m\),他希望你将序列 \(b\) 划分为 \(k\) 段连续非空子序列,使得每一段的异或和之和最小。即,他想知道在所有满足 \(0 = p_0 < p_1 < \cdots < p_k = m\) 条件的序列 \(p\) 中下式的最小值: $$ \sum_{i=1}^{k} (b_{p_{i-1} + 1} \mathbin{\mathrm{xor}} \cdots \mathbin{\mathrm{xor}} b_{p_i}) $$ 这个最小值即称为这个序列的妙妙值

但是这个问题非常简单,于是出题人 01 找来了一个长度恰好为 \(n\) 的非负整数序列 \(a_1, \dots, a_n\)。他想考考你,\(a\) 的每个前缀 \(a_1, \dots, a_j\)\(k \le j \le n\))的妙妙值分别是多少呢?

对于所有测试点,满足 \(1 \le k \le n \le 60000, k \le 8,a_i < 2^{16}\)

题解

显然的DP状态\(f(i,j)\)表示前\(j\)个数分\(i\)段的最小代价。

考虑这样一个暴力,把\(f(i-1,j)\)存在\(g(j)\)里,然后转移\(f(i,j)\)的时候枚举\(k\)\(g(k)\)转移。时间复杂度\(O(knv)\)

本质上这是\(O(1)\)修改,\(O(v)\)查询。现在我们尝试平衡一下两部分的复杂度。

把前\(8\)位的异或贡献放到修改的时候做,把后\(8\)位的异或贡献放到查询的时候做。这样就能做到\(O(kn\sqrt{v})\)

CO int N=6e4+10,inf=1e9;
int a[N],g[1<<8][1<<8],f[9][N];

int main(){
	int n=read<int>(),m=read<int>();
	for(int i=1;i<=n;++i) a[i]=a[i-1]^read<int>();
	f[1][0]=inf,copy(a+1,a+n+1,f[1]+1);
	for(int k=2;k<=m;++k){
		for(int x=0;x<1<<8;++x) fill(g[x],g[x]+(1<<8),inf);
		for(int i=0;i<=n;++i){
			f[k][i]=inf;
			for(int x=0;x<1<<8;++x) f[k][i]=min(f[k][i],g[a[i]>>8][x]+((a[i]&((1<<8)-1))^x));
			for(int x=0;x<1<<8;++x) g[x][a[i]&((1<<8)-1)]=min(g[x][a[i]&((1<<8)-1)],f[k-1][i]+((a[i]>>8^x)<<8));
		}
	}
	for(int i=m;i<=n;++i) write(f[m][i]," \n"[i==n]);
	return 0;
}

网络恢复

这是一道交互题。

出题人 02 喜欢网上冲浪。可是这天,他所在的小区的网络坏掉了,于是他喊来了你帮忙修一修。

小区的网络由 \(N\) 个网络结点和 \(M\) 条信道组成,可以被看作是一张 \(N\) 个点 \(M\) 条边的无向简单图(简单图满足任意两点之间至多存在一条直接相连的边,且没有自环)。点从 \(1 \sim N\) 编号,边从 \(1 \sim M\) 编号。目前,你只知道信道的总数是 \(M\),并且还掌握着每条信道的管理权限,然而你并不知道每条信道连接着哪两个结点。

为了恢复出网络结构,你可以使用一种土办法:重启大法!

当然重启也是需要智慧的。具体来说,你可以进行若干次操作,每次操作方式如下:

  1. 给每个结点 \(i\) 标上一个自己定的权值 \(a_i\)

  2. 选取一个信道的子集 \(S\),把不在 \(S\) 里的信道都关闭,只让 \(S\) 里的信道保持开启状态;

  3. 此时,每个结点 \(i\) 会自动计算出与 \(i\) 通过开启状态的信道直接相邻的所有点 \(v\)\(a_v\) 异或和,记为 \(b_i\)

  4. 你通过管理员权限获取所有结点的 \(b_i\) 值,然后关闭的信道都重启,网络恢复至原状。

请你在不超过 \(50\) 次操作内,求出所有信道构成的集合。

注意,你只需要求出信道的集合。即,你只需要恢复出哪些结点之间有信道,不用恢复出每条信道对应的编号。

题解

每轮随机取出一些边,只考虑被取出的边。每条边在不同的轮中可以被重复取出。使用剥叶子的方法持续找出度为\(1\)的点,直到剩下每个点的度都至少为\(2\)。每条边有玄学的概率在至少一轮中被发现了。能得80分。

CO int N=5e4+10,M=3e5+10;
uint64 a[N],b[N];
int p[M];
unordered_map<uint64,int> f;
int que[2*N];
set<pair<int,int> > e;

void report(int x,int y){
	if(x>y) swap(x,y);
	if(e.count({x,y})) return;
	e.insert({x,y});
	Report(x,y);
}
void solve(int n){
	int l=1,r=n;
	for(int i=1;i<=n;++i) que[i]=i;
	while(l<=r){
		int x=que[l++];
		if(!f.count(b[x])) continue;
		int y=f[b[x]];
		report(x,y);
		b[y]^=a[x],b[x]=0;
		que[++r]=y;
	}
}
void Solve(int n,int m){
	srand(20030506);
	for(int i=1;i<=n;++i) a[i]=gen(),f[a[i]]=i;
	iota(p+1,p+m+1,1);
	random_shuffle(p+1,p+m+1);
	int len=(m+49)/50;
	for(int t=1;t<=50;++t){
		vector<uint64> b=Query(vector<uint64>(a+1,a+n+1),vector<int>(p+(t-1)*len+1,p+min(t*len,m)+1));
		copy(b.begin(),b.end(),::b+1);
		solve(n);
	}
}

稍微加点优化,就能得到100分。考察选手乱搞能力。

CO int base=(1<<16)-1;
mt19937_64 gen(20030506);
int n,m;
set<pair<int,int> > e;

IN void report(int x,int y){
	e.insert(minmax(x,y));
}
void solve(vector<int> s){
	vector<uint64> a(n);
	for(int i=0;i<n;++i){
		a[i]=gen()>>16<<16; // random number for leaf
		if(gen()&1) a[i]|=i; // identity number for circle
	}
	vector<uint64> b=Query(a,s);
	unordered_map<uint64,int> f;
	for(int i=0;i<n;++i) f[a[i]]=i;
	deque<int> q;
	for(int i=0;i<n;++i)if(f.count(b[i])) q.push_back(i); // deg=1
	for(int t=0;t<1000;++t){
		while(q.size()){
			int x=q.front();q.pop_front();
			if(!b[x]) continue;
			int y=f[b[x]];
			report(x,y);
			b[x]=0;
			if(b[y]!=a[x] and b[y]){
				b[y]^=a[x];
				if(f.count(b[y])) q.push_back(y);
			}
		}
		vector<int> rem;
		for(int i=0;i<n;++i)if(b[i]) rem.push_back(i);
		if(rem.empty()) break;
		shuffle(rem.begin(),rem.end(),gen);
		int any=0;
		function<void(int,int)> judge=[&](int x,int y)->void{
			if(b[x] and b[y] and f.count(b[x]^a[y])){
				report(x,y);
				b[x]^=a[y];
				b[y]^=a[x];
				q.push_back(x);
				if(f.count(b[y])) q.push_back(y);
				any=1;
			}
		};
		for(int x:rem){
			int y=b[x]&base;
			if(0<=y and y<n) judge(x,y);
		}
		if(!any){
			for(int i=0;i<(int)rem.size()*10;++i){
				int x,y;
				do x=gen()%rem.size(),y=gen()%rem.size();
				while(x==y);
				judge(rem[x],rem[y]);
			}
		}
		if(!any) break;
	}
}
void Solve(int n,int m){
	::n=n,::m=m;
	int c=n*0.8;
	for(int i=0;i<m;i+=c){
		int need=min(i+c,m);
		vector<int> s(need-i);
		iota(s.begin(),s.end(),i+1);
		solve(s);
		while((int)e.size()<need){
			vector<int> s0,s1;
			for(int x:s) gen()&1?s0.push_back(x):s1.push_back(x);
			solve(s0);
			if((int)e.size()==need) break;
			solve(s1);
		}
	}
	for(CO pair<int,int>&p:e) Report(p.first+1,p.second+1);
}

校园闲逛

为了出题,出题人 03 喜欢在校园里闲逛。

校园可以看成抽象成是一张 \(n\) 个点 \(m\) 条边的有向图,第 \(i\) 条边从 \(a_i\) 连向 \(b_i\),边权为 \(c_i\)

出题人 03 总共会闲逛 \(Q\) 天,在第 \(i\) 天,他会从 \(x_i\) 出发,到达 \(y_i\),同时希望自己走过的路径边权和恰好为 \(v_i\)

出题人 03 很好奇,每一天他可以有多少种不同的路径呢? 由于答案很大,你只需要回答答案对 \(998244353\) 取模的结果。

同一条路径可以多次经过同一条边。两条路径相同当且仅当两条路径上的边数相同且边的编号依次相等。

对于所有测试点,满足 \(1 \le n \le 8,0 \le m \le 300000,1 \le \max_v \le 65000,0 \le Q \le 10000\)

题解

矩阵多项式求逆模板题(误)。

需要注意求逆过程中矩阵乘法的顺序。

struct matrix {int x[8][8];} e;

void init_matrix(){
	for(int i=0;i<8;++i) e.x[i][i]=1;
}
matrix operator+(matrix a,CO matrix&b){
	for(int i=0;i<8;++i)for(int j=0;j<8;++j)
		a.x[i][j]=add(a.x[i][j],b.x[i][j]);
	return a;
}
matrix operator-(matrix a,CO matrix&b){
	for(int i=0;i<8;++i)for(int j=0;j<8;++j)
		a.x[i][j]=add(a.x[i][j],mod-b.x[i][j]);
	return a;
}
matrix operator*(matrix a,int b){
	for(int i=0;i<8;++i)for(int j=0;j<8;++j)
		a.x[i][j]=mul(a.x[i][j],b);
	return a;
}
matrix operator*(CO matrix&a,CO matrix&b){
	matrix ans={};
	for(int k=0;k<8;++k)
		for(int i=0;i<8;++i)for(int j=0;j<8;++j)
			ans.x[i][j]=add(ans.x[i][j],mul(a.x[i][k],b.x[k][j]));
	return ans;
}

typedef vector<matrix> poly;
CO int N=1<<17;
int omg[2][N],rev[N];

void init_poly(){
	omg[0][0]=1,omg[0][1]=fpow(3,(mod-1)/N);
	omg[1][0]=1,omg[1][1]=fpow(omg[0][1],mod-2);
	rev[0]=0,rev[1]=1<<16;
	for(int i=2;i<N;++i){
		omg[0][i]=mul(omg[0][i-1],omg[0][1]);
		omg[1][i]=mul(omg[1][i-1],omg[1][1]);
		rev[i]=rev[i>>1]>>1|(i&1)<<16;
	}
}
template<bool dir>
void FFT(poly&a){
	int lim=a.size(),len=log2(lim);
	for(int i=0;i<lim;++i){
		int r=rev[i]>>(17-len);
		if(i<r) swap(a[i],a[r]);
	}
	for(int i=1;i<lim;i<<=1)
		for(int j=0;j<lim;j+=i<<1)for(int k=0;k<i;++k){
			matrix t=a[j+i+k]*omg[dir][N/(i<<1)*k];
			a[j+i+k]=a[j+k]-t,a[j+k]=a[j+k]+t;
		}
	if(dir){
		int ilim=fpow(lim,mod-2);
		for(int i=0;i<lim;++i) a[i]=a[i]*ilim;
	}
}
poly operator~(poly a){
	int n=a.size();
	poly b={e};
	a.resize(1<<(int)ceil(log2(n)));
	for(int lim=2;lim<2*n;lim<<=1){
		poly c(a.begin(),a.begin()+lim);
		c.resize(lim<<1),FFT<0>(c);
		b.resize(lim<<1),FFT<0>(b);
		for(int i=0;i<lim<<1;++i) b[i]=b[i]*(e*2-c[i]*b[i]);
		FFT<1>(b),b.resize(lim);
	}
	return b.resize(n),b;
}

int main(){
	init_matrix();
	init_poly();
	int n=read<int>(),m=read<int>(),q=read<int>(),lim=read<int>();
	poly f(lim+1);
	f[0]=e;
	while(m--){
		int x=read<int>()-1,y=read<int>()-1,w=read<int>();
		f[w].x[x][y]=add(f[w].x[x][y],mod-1);
	}
	f=~f;
	while(q--){
		int x=read<int>()-1,y=read<int>()-1,w=read<int>();
		write(f[w].x[x][y],'\n');
	}
	return 0;
}

posted on 2020-08-12 22:30  autoint  阅读(298)  评论(0编辑  收藏  举报

导航