1. 程式人生 > 實用技巧 >UOJ NOI Round 4

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;
}