hijklmn

黑暗料理

题意简述:给定一个长度为 \(n\) 的序列,删掉一些数,使得在剩下的序列中任选两个数 \(x,y\) 使得 \(x+y\) 都为非质数,求最多剩下多少数。\((n\le 750)\)

首先显然最终剩下的序列中最多有一个 \(1\)

再考虑到所有偶数都不是质数,可以把序列分成奇数和偶数两部分,用 Miller-Rabin 判断两数之和是否为质数,若是则将这两个数连边,得到的其实是一张二分图。

那么就转换为选最多的点,满足两两之间没有边相连。也就是求这张二分图的最大独立集。

点击查看代码
#include<bits/stdc++.h>
using ll=long long;
const int inf=0x3f3f3f3f;
inline ll qpow(ll a,ll b,ll p){ll ans(1);for(;b;b>>=1,a=a*a%p)if(b&1)ans=ans*a%p;return ans;} 
const short prime[]={2,7,61};
inline bool check(int x,int a){
	int d=x-1,t=qpow(a,d,x);
	if(t!=1)return false;
	for(d>>=1;!(d&1);d>>=1){
		t=qpow(a,d,x);
		if(t==x-1)return true;
		if(t!=1)return false;
	}
	return true;
}
inline bool MR(int x){
	if(x==2||x==7||x==61)return true;
	if(!(x&1))return false;
	for(int a:prime)if(!check(x,a))return false;
	return true;
}
int n,s,t,a[800];
struct Maxflow{
	struct node{int to,next,flow;}a[350000];
	int o=1,head[800],cur[800],dis[800],q[800],hd,tl;
	inline void clear(){o=1;memset(head,0,sizeof head);memset(cur,0,sizeof cur);memset(a,0,sizeof a);}
	inline void add(int x,int y,int c){
		a[++o]=(node){y,head[x],c};head[x]=o;
		a[++o]=(node){x,head[y],0};head[y]=o;
	}
	inline bool bfs(){
		for(int i=0;i<=t;++i)cur[i]=head[i],dis[i]=0;
		q[hd=tl=1]=s,dis[s]=1;
		while(hd<=tl){
			int x=q[hd++];
			for(int i=head[x],y;i;i=a[i].next){
				if(a[i].flow&&!dis[y=a[i].to]){
					dis[y]=dis[x]+1;
					q[++tl]=y;
					if(y==t)return true;
				}
			}
		}
		return false;
	}
	int dfs(int x,int flow){
		if(!flow||x==t)return flow;
		int fl,tmp=0;
		for(int i=cur[x],y;i;i=a[i].next){
			cur[x]=i;
			if(a[i].flow&&dis[y=a[i].to]==dis[x]+1&&(fl=dfs(y,std::min(flow-tmp,a[i].flow)))){
				a[i].flow-=fl,a[i^1].flow+=fl,tmp+=fl;
				if(!(flow-tmp))break;
			}
		}
		return tmp;
	}
	inline int dinic(){
		int maxflow=0;
		while(bfs())maxflow+=dfs(s,inf);
		return maxflow;
	}
}fl;
namespace Koishi{
	static int p1[800],p2[800];
	inline void solve(){
		fl.clear();
		std::cin>>n;
		int cnt1=0,cnt2=0;
		for(int i=1;i<=n;++i)std::cin>>a[i];
		std::sort(a+1,a+1+n);
		if(a[1]==1)p1[++cnt1]=1;
		for(int i=1;i<=n;++i){
			if(a[i]==1)continue;
			if(a[i]&1)p1[++cnt1]=a[i];
			else p2[++cnt2]=a[i];
		}
		s=0,t=cnt1+cnt2+1;
		for(int i=1;i<=cnt1;++i)fl.add(s,i,1);
		for(int i=1;i<=cnt2;++i)fl.add(cnt1+i,t,1);
		for(int i=1;i<=cnt1;++i){
			for(int j=1;j<=cnt2;++j){
				if(MR(p1[i]+p2[j]))fl.add(i,cnt1+j,inf);
			}
		}
		std::cout<<(cnt1+cnt2-fl.dinic())<<'\n';
	}
}
int main(){
	freopen("cooking.in","r",stdin);freopen("cooking.out","w",stdout); 
	std::cin.tie(nullptr)->sync_with_stdio(false);
	int T;std::cin>>T;while(T--)Koishi::solve();
	return 0;
}

害怕

题意简述: 有 \(n\) 个点 \(m\) 条边的无向图,每条边为蓝色或白色,要给每条边赋上边权,边权为 \(1\sim m\) 的排列,要求蓝色的边为图的最小生成树(保证有解),求字典序最小的一组解。\((n,m\le 5\times 10^5)\)

贪心地去考虑,从前往后依次处理每条边,把边权小的尽可能往前放。若当前边是蓝边,就直接赋值。若当前边是白边,要满足蓝边形成 MST,所以要保证这条白边与蓝边形成的环中,白边的权值是最大的。那么就要把夹在中间尚未赋值的蓝边按编号依次赋值,再把白边赋上值。暴力跳是 \(O(n^2)\) 的。考虑到赋过值的边没有作用,就可以用并查集把树上已赋值的边缩起来,可以做到 \(O(n\alpha(n))\)(挂分原因:注意到可能有重边,不能直接用两个端点代表一条边)

点击查看代码
#include<bits/stdc++.h>
#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/hash_policy.hpp>
using ll=long long;
using ull=unsigned long long;
int n,m,cnt;
struct edge{int u,v,c;}e[500005];
std::vector<int> g[500005],tmpid,tmpval;
std::queue<int> q;
__gnu_pbds::gp_hash_table<int,ull> h;
__gnu_pbds::gp_hash_table<ull,int> p;
std::mt19937_64 rnd(std::chrono::steady_clock::now().time_since_epoch().count());
int ans[500005],fa[500005],dep[500005],F[500005],root;
int getfa(int x){return F[x]==x?x:F[x]=getfa(F[x]);}
void dfs(int x,int f){
	dep[x]=dep[f]+1,fa[x]=f;
	for(int &y:g[x]){
		if(y==f)continue;
		dfs(y,x);
	}
}
int main(){
	std::cin.tie(nullptr)->sync_with_stdio(false);
	std::cin>>n>>m;
	for(int i=1;i<=n;++i)h[i]=rnd();
	for(int i=1,u,v,c;i<=m;++i){
		q.push(i);
		std::cin>>u>>v>>c;
		p[h[u]*h[v]+c]=i;
		e[i]=(edge){u,v,c};
		if(c==1){
			if(!root)root=u;
			g[u].push_back(v);
			g[v].push_back(u);
		}
	}
	std::iota(F+1,F+1+n,1);
	dfs(root,0);
	for(int i=1;i<=m;++i){
		int x=e[i].u,y=e[i].v;
		int fx=getfa(x),fy=getfa(y);
		if(e[i].c==1){
			if(!ans[i])ans[i]=q.front(),q.pop();
			if(fx!=fy){
				if(dep[fx]>dep[fy])std::swap(fx,fy);
				F[fy]=fx;
			}
		}
		else{
			tmpid.clear();
			tmpval.clear();
			while(fx!=fy){	
				if(dep[fx]<dep[fy])std::swap(x,y),std::swap(fx,fy);
				tmpid.push_back(p[h[fx]*h[fa[fx]]+1]);
				tmpval.push_back(q.front());
				q.pop();
				x=fx,fx=getfa(fa[x]);
				F[x]=fx;
			}
			std::sort(tmpid.begin(),tmpid.end());
			for(int j=0;j<tmpid.size();++j)ans[tmpid[j]]=tmpval[j];
			ans[i]=q.front();q.pop();
		}
	}
	for(int i=1;i<=m;++i)std::cout<<ans[i]<<' ';
	return 0;
}

博弈树

题意简述:给一棵 \(n\) 个点的树,A 和 B 轮流移动一颗棋子到树上任意位置,要求每次移动要比对方上一次移动距离大,A先手。\(q\) 次询问,每次询问给定起点 \(u\),问谁会获胜。\((n,q\le 10^5)\)

树上任意两点最长的距离是树的直径,所以一方如果在直径端点上,只要移动到另一个端点,就一定可以获胜。考虑将不断将所有直径端点一层层删去,最后剩下的那个点是先手必败的,因为它无论如何都会移动到直径端点上去,其他情况都是先手必胜。而剩下的这个点就是直径中点。

点击查看代码
#include<bits/stdc++.h>
int n,q;
std::vector<int> g[100005];
int dep[100005],son[100005],siz[100005],fa[100005],m1,m2,maxd;
void dfs1(int x,int f){
	dep[x]=dep[f]+1,siz[x]=1,fa[x]=f;
	if(dep[m1]<dep[x])m1=x;
	for(int &y:g[x]){
		if(y==f)continue;
		dfs1(y,x);
		siz[x]+=siz[y];
		if(siz[y]>siz[son[x]])son[x]=y;
	}
}
int top[100005],id[100005],tot;
void dfs2(int x,int t){
	top[x]=t,id[x]=++tot;
	if(!son[x])return;
	dfs2(son[x],t);
	for(int &y:g[x])if(y!=fa[x]&&y!=son[x])dfs2(y,y);
}
void dfs3(int x,int f,int d){
	if(maxd<=d)maxd=d,m2=x;
	for(int &y:g[x]){
		if(y==f)continue;
		dfs3(y,x,d+1);
	}
}
inline int lca(int x,int y){
	while(top[x]^top[y]){
		if(dep[top[x]]<dep[top[y]])std::swap(x,y);
		x=fa[top[x]];
	}
	return dep[x]<dep[y]?x:y;
}
inline int getdist(int x,int y){return dep[x]+dep[y]-2*dep[lca(x,y)];}
int main(){
	freopen("tree.in","r",stdin);freopen("tree.out","w",stdout);
	std::cin.tie(nullptr)->sync_with_stdio(false);
	std::cin>>n>>q;
	for(int i=1,u,v;i<n;++i)std::cin>>u>>v,g[u].push_back(v),g[v].push_back(u);
	dfs1(1,0);dfs2(1,1);dfs3(m1,0,0);
	std::cerr<<maxd<<' '<<m1<<' '<<m2<<'\n';
	for(int u;q;--q){
		std::cin>>u;
		int dis=std::max(getdist(u,m1),getdist(u,m2));
		if(dis>maxd/2)std::cout<<"Alice\n";
		else std::cout<<"Bob\n";
	}
	return 0;
}

划分

题意简述:长度为 \(n\) 的 01 串 \(S\),求将其分成至少 \(k\) 段,把每一段看成二进制数求和后的最大值,以及取到这个最大值的划分数量。

首先想到要尽可能最大化某一段的长度,这样得到的值越大。分情况讨论,若前导零个数满足大于等于 \(k\),则最大值一定是后面分成的一整个大段,方案数就是把前面 \(cnt\) 个 0 分成大于等于 \(k\) 段的方案数。插板法可以求得答案为 \(\sum_{i\ge k} {cnt\choose i-1}\)。再考虑其他情况,我们希望把序列分成 \(k-1\) 段长度的和一段长度为 \(n-k+1\) 的,设其为 \(p\)。最终答案就为 \(p+\text{popcount}(S)-\text{popcount}(p)\)。我们的目标就是最大化长度为 \(n-k+1\) 的段,可以二分哈希快速比较,复杂度 \(O(n\log n)\)。注意当 \(p\) 末位是 1 时,\(p-1\) 也为一种合法的划分,因为会和 \(\text{popcount}(p)\) 抵消贡献,不要漏算方案数。

点击查看代码
#include<bits/stdc++.h>
using ll=long long;
const int P=998244353;
int n,k;
int fac[2000005],inv[2000005];
inline ll qpow(ll a,ll b){ll ans(1);for(;b;b>>=1,a=a*a%P)if(b&1)ans=ans*a%P;return ans;}
inline ll C(ll x,ll y){return x<y?0:(ll)fac[x]*inv[y]%P*inv[x-y]%P;}
char s[2000005];
ll h[2000005],base[2000005];
inline ll gethash(int x,int len){return (h[x+len-1]-h[x-1]*base[len]%P+P)%P;}
int main(){
	freopen("divide.in","r",stdin);freopen("divide.out","w",stdout);
	std::cin.tie(nullptr)->sync_with_stdio(false);
	std::cin>>n>>k;
	for(int i=1;i<=n;++i)std::cin>>s[i];
	int p=1,sum=0,cnt=0;
	if(n==k){
		for(int i=1;i<=n;++i)sum+=(s[i]=='1');
		std::cout<<sum<<' '<<1;
		exit(0);
	}
	fac[0]=inv[0]=1;
	for(int i=1;i<=n;++i)fac[i]=(ll)fac[i-1]*i%P;
	inv[n]=qpow(fac[n],P-2);
	for(int i=n-1;i;--i)inv[i]=(ll)inv[i+1]*(i+1)%P;
	while(p<=n&&s[p]=='0')p++;
	if(p>k){
		for(int i=p;i<=n;++i)sum=(2ll*sum%P+(s[i]=='1'))%P;
		if(p==n+1)p--;
		for(int i=k-1;i<p;++i)cnt=(cnt+C(p-1,i))%P;
		std::cout<<sum<<' '<<cnt;
		exit(0);
	}
	for(int i=1;i<=n;++i)h[i]=(h[i-1]*131%P+s[i])%P;
	base[0]=1;
	for(int i=1;i<=n;++i)base[i]=base[i-1]*131%P;
	int now=1;cnt=1;
	for(int i=2;i<=k;++i){
		int l=0,r=n-k+1,mid,ans;
		while(l<=r){
			mid=l+r>>1;
			if(gethash(now,mid)==gethash(i,mid))l=mid+1,ans=mid;
			else r=mid-1;
		}
		if(ans>=n-k){
			cnt++;
			continue;
		}
		if(s[now+ans]<s[i+ans]){
			now=i;
			cnt=1;	
		}
	}
	for(int i=now;i<=now+n-k;++i)sum=(sum*2%P+(s[i]=='1'))%P;
	for(int i=1;i<now;++i)sum+=(s[i]=='1');
	for(int i=now+n-k+1;i<=n;++i)sum+=(s[i]=='1');
	sum%=P;
	std::cout<<sum<<' '<<cnt;
	return 0;
}

旅行

题意简述:\(n\) 个点 \(n\) 条边的无向连通图每条边有初始颜色,\(m\) 次操作 \(x,y,c\) 表示将连接 \(x,y\) 的边染成 \(c\) 颜色(保证连接 \(x,y\) 的边是原图的边),求每次操作之后有多少颜色相同的连通块。\((n,m\le 10^5)\)

初始图是一棵基环树。我们可以用哈希表维护每个点的出边颜色种类及个数。染色修改操作可以看成先删后加,这样就可以轻松维护连通块数量。但还需要特殊处理环上的边,若环上都为一种颜色时,需要额外加上多减去的 1。

点击查看代码
namespace Flandreqwq{
	int n,m,o,sum[100005],head[100005],dep[100005],stk[100005],cir[100005],iscir[200005],top,cnt,ans;
	hash_table col[100005],id[100005];
	struct E{int to,next,w;}e[200005];
	inline void add(int x,int y,int z){
		e[++o]={y,head[x],z};head[x]=o;
		id[x][y]=o;
	}
	void dfs(int x,int f){
		dep[x]=dep[f]+1;
		stk[++top]=x;
		for(int i=head[x],y;i;i=e[i].next){
			if((y=e[i].to)==f)continue;
			if(dep[y]&&dep[y]<dep[x]){
				while(stk[top]!=y)cir[++cnt]=stk[top--];
				cir[++cnt]=y;
			}
			else if(!dep[y]){
				dfs(y,x);
			} 
		}
		if(stk[top]==x)top--;
	}
	inline void clear(){
		for(int i=1;i<=n;++i){
			col[i].clear();
			id[i].clear();
			sum[i]=head[i]=dep[i]=0;
		}
		for(int i=1;i<=o;++i)iscir[i]=0;
		o=1,cnt=0,top=0,ans=0;
	}
	inline void ins(int x,int y,int c,int pd){
		if(!col[x][c]&&!col[y][c])ans++;
		if(col[x][c]&&col[y][c])ans--;
		col[x][c]++,col[y][c]++;
		if(pd)sum[c]++;
		if(pd&&sum[c]==cnt)ans++;
	}
	inline void del(int x,int y,int c,int pd){
		col[x][c]--,col[y][c]--;
		if(!col[x][c]&&!col[y][c])ans--;
		if(col[x][c]&&col[y][c])ans++;
		if(pd&&sum[c]==cnt)ans--;
		if(pd)sum[c]--;
	}
	inline void solve(){
		clear();
		std::cin>>n>>m;
		for(int i=1,x,y,c;i<=n;++i){
			std::cin>>x>>y>>c;
			add(x,y,c),add(y,x,c);
		}
		dfs(1,0);
		for(int i=1;i<=cnt;++i)iscir[id[cir[i]][cir[i%cnt+1]]]=iscir[id[cir[i]][cir[i%cnt+1]]^1]=1;
		for(int i=2;i<=o;i+=2)ins(e[i].to,e[i^1].to,e[i].w,iscir[i]);
		for(int i=1,x,y,c;i<=m;++i){
			std::cin>>x>>y>>c;
			int p=id[x][y];
			del(x,y,e[p].w,iscir[p]);
			ins(x,y,c,iscir[p]);
			e[p].w=e[p^1].w=c;
			std::cout<<ans<<'\n';
		}
	}
	void main(){
		int testcases;
		std::cin>>testcases;
  		while(testcases--)solve();
	}
}

异或

题意简述:给定长度为 \(n\) 的数组 \(a\),每次选一个区间 \([l,r]\),将 \(a_{[l,r]}\) 异或上 \(w\),求将 \(a_i\) 全变为 0 的最少操作数。\((n\le 17,0 \le a_i\le 10^{18})\)

求出 \(a\) 的差分序列 \(b\),这样就将区间操作转化为了对差分序列的双点操作或者单点操作(取后缀)。最终 \(b\) 序列也会全变为 0。考虑将每次修改位置连边,最终会得到若干个连通块,设连通块大小为 \(x\),则其边数上界为 \(x\)(一堆双点操作加上一个单点一定行),下界为 \(x-1\)。而且取到下界时当且仅当连通块内的数异或和为 0。必要性:考虑到一定都是双点操作,连通块内的异或和不会因操作发生改变,最后是 0 所以初始一定是 0。充分性:每次异或掉开头的数,\(x-1\) 次操作后最后一定剩下 0。设会有 \(y\) 个连通块边数取到下界,那么答案就是 \(n-y\)。我们就要最大化 \(y\),问题转换为,我们需要求差分序列异或和为 0 的子序列的最多个数。状压后枚举子集转移即可 \(f_s=f_{s \oplus t}+1 (t\subset s \wedge \text{t 内异或和为 0})\)。时间复杂度 \(O(3^n)\)

点击查看代码
	std::cin>>n;
	for(int i=1;i<=n;++i)std::cin>>a[i];
	for(int i=1;i<=n;++i)b[i]=a[i]^a[i-1];
	maxn=1<<n;
	for(int s=1;s<maxn;++s){
		ll sum=0;
		for(int i=1;i<=n;++i){
			if(s&(1<<i-1))sum^=b[i];
		}
		if(!sum)f[s]=true;
	}
	for(int s=1;s<maxn;++s){
		for(int t=s;t;t=(t-1)&s){
			if(f[t])g[s]=std::max(g[s],g[s^t]+1);
		}
	}
	std::cout<<n-g[maxn-1];

差后队列

题意简述:一个队列,支持两种操作,正常插入一个数,随机删除一个不是最大值的数(若只有一个数就删除该数),对于每个插入操作求出该数期望在哪一步被删除的,对于每个删除操作求出被删除的期望大小。

不是最大值的数是等价的。从前往后处理如果一个数不是最大值就把它加入贡献,维护删掉数后的元素个数为 \(siz\),遇到删除操作答案就为\(\frac{sum}{siz}\),之后 \(sum\to 1-\frac{sum}{siz}\)。删除操作成功删掉一个非最大值的概率是 \(\frac{1}{siz}\)。对于一个数,它被删除的时间期望是在后面删除操作上被删除的期望和,这个数在一个删除操作还要考虑后面的数都没被删掉的概率,从后往前处理并更新答案即可。时间复杂度 \(O(n)\)

点击查看代码
	std::cin>>n;
	for(int i=1,opt;i<=n;++i){
		std::cin>>opt;
		if(!opt){
			cnt++;
			std::cin>>a[i];
			if(mx==0)mx=i;
			else if(a[mx]<a[i]){
				Modadd(sum,a[mx]);
				pre[i]=mx;
				mx=i;
			}
			else{
				Modadd(sum,a[i]);
				pre[i]=i;
			}
		}
		else{
			cnt--;
			if(cnt){
				Modmul(sum,Inv(cnt));
				ans[i]=sum;
				Modmul(sum,cnt-1);
			}
			else{
				ans[mx]=i;
				ans[i]=a[mx];
				mx=0;
			}
		}
		siz[i]=cnt;
	}
	int k=0;
	for(int i=n;i;--i){
		if(a[i]){
			ans[pre[i]]=k;
		}
		else{
			k=Add(Mul(i,Inv(siz[i])),Mul(k,Dec(1,Inv(siz[i]))));
		}
	}
	for(int i=1;i<=n;++i)std::cout<<ans[i]<<' ';

高爸

题意简述:给定 \(n\) 个数的序列 \(a\),你有两种操作,使 \(a_i\) 加上 1 或者减去 1,代价均为 1。对于每一个 \(i\),求使得序列前 \(i\) 个数都相等的最小花费。

可以枚举最终可能变为的数,取最小值,这样是 \(O(n^2)\) 的。类似于若干绝对值函数相加,不难发现这个函数是单峰的,可以在线段树上二分,同时维护大于和小于 \(x\) 的数的个数与权值和,根据 \(f_mid\)\(f_{mid+1}\) 的关系来判断进入哪个儿子。

点击查看代码
#define int long long
int n,a,b,c[100005],suma,cnta,sumb,cntb;
int root;
struct SegmentTree{
	struct node{int ls,rs,cnt,sum;}T[3200005];
	int tot=0;
	void pushup(int o){
		T[o].cnt=T[T[o].ls].cnt+T[T[o].rs].cnt;
		T[o].sum=T[T[o].ls].sum+T[T[o].rs].sum;
	}
	void modify(int &o,int l,int r,int x){
		if(!o)o=++tot;
		if(l==r){T[o].sum+=x,T[o].cnt++;return;}
		int mid=l+r>>1;
		x<=mid?modify(T[o].ls,l,mid,x):modify(T[o].rs,mid+1,r,x);
		pushup(o);
	}
	int f(int x){return a*(cnta*x-suma)+b*(sumb-cntb*x);};
	void query(int o,int l,int r){
		if(l==r){
			suma+=T[o].sum,cnta+=T[o].cnt;
			std::cout<<f(l)<<'\n';
			return;
		}
		int mid=l+r>>1;
		suma+=T[T[o].ls].sum,cnta+=T[T[o].ls].cnt;
		sumb+=T[T[o].rs].sum,cntb+=T[T[o].rs].cnt;
		if(f(mid)<f(mid+1)){
			suma-=T[T[o].ls].sum,cnta-=T[T[o].ls].cnt;
			query(T[o].ls,l,mid);
		}
		else{
			sumb-=T[T[o].rs].sum,cntb-=T[T[o].rs].cnt;
			query(T[o].rs,mid+1,r);
		}
	}
}t;
main(){
	std::cin.tie(nullptr)->sync_with_stdio(false);
	time_st=clock();
	freopen("c.in","r",stdin);
	freopen("c.out","w",stdout);
	std::cin>>n>>a>>b;
	for(int i=1;i<=n;++i)std::cin>>c[i];
	int mx=*std::max_element(c+1,c+1+n);
	for(int i=1;i<=n;++i){
		t.modify(root,0,mx,c[i]);
		suma=cnta=sumb=cntb=0;
		t.query(root,0,mx);
	}
	time_ed=clock();
	std::cerr<<"Time:"<<((time_ed-time_st)/CLOCKS_PER_SEC)<<"s"<<std::endl;
	return 0;
}

金牌

题意简述:一棵树,长度为 \(d\) 的路径权值为 \(2^d\)\(q\) 次询问 \(x,y\),求所有通过顶点 \(x\)\(y\) 的简单路径权值之和。

先预处理求出每一个 \(i\) 子树内所有 \(2^d\) 的权值和 \(f_i\),先钦定 \(dep_x < dep_y\),分情况讨论,若 \(x\) 不是 \(y\) 的祖先,\(x,y\) 都能取遍子树节点,答案为 $f_x\times f_y\times dis(x,y) $。若 \(x\)\(y\) 的祖先,\(y\) 可选子树节点,\(x\) 可以选整棵树除掉 \(x\)\(y\) 方向上的儿子 \(z\) 的子树。可以换根 dp 预处理出以 \(i\) 为根的权值 \(g_i\)。答案就为 \(f_y\times (g_x-f_z)\times dis(x,y)\)。树剖求 lca 的话时间复杂度为 \(O(n\log n)\),轻微卡常。

点击查看代码
fastio::IN fin;
fastio::OUT fout;
const int N=1000000;
int n,q,base[N+5],dep[N+5],siz[N+5],fa[N+5],son[N+5],top[N+5],id[N+5],val1[N+5],val2[N+5],tot;
struct node{int to,next;}g[N<<1|1];int head[N+5],o;
inline void add(int x,int y){g[++o]={y,head[x]};head[x]=o;}
void dfs1(int x,int f){
	dep[x]=dep[f]+1,siz[x]=1,fa[x]=f;
	for(int i=head[x],y;i;i=g[i].next){
		if((y=g[i].to)^f){
			dfs1(y,x); 
			siz[x]+=siz[y];
			if(siz[y]>siz[son[x]])son[x]=y;	
		}
	}
}
void dfs2(int x,int t){
	top[x]=t,id[x]=++tot;
	if(!son[x])return;
	dfs2(son[x],t);
	for(int i=head[x],y;i;i=g[i].next){
		if((y=g[i].to)^fa[x]&&y^son[x])dfs2(y,y);
	}
}
void dfs3(int x,int f){
	val1[x]=1;
	for(int i=head[x],y;i;i=g[i].next){
		if((y=g[i].to)^f){
			dfs3(y,x);
			Modadd(val1[x],Mul(2,val1[y]));	
		}
	}
}
void dfs4(int x,int f){
	for(int i=head[x],y;i;i=g[i].next){
		if((y=g[i].to)^f){
			val2[y]=Add(val1[y],Mul(2,Dec(val2[x],Mul(2,val1[y]))));
			dfs4(y,x);
		}
	}
}
inline int lca(int x,int y){
	for(;top[x]^top[y];x=fa[top[x]])if(dep[top[x]]<dep[top[y]])std::swap(x,y);
	return dep[x]<dep[y]?x:y;
}
inline int getdist(int x,int y){return dep[x]+dep[y]-2*dep[lca(x,y)];}
inline int find(int x,int y){
	while(dep[x]>dep[y]){
		if(fa[top[x]]==y)return top[x];
		x=fa[top[x]];
	}
	return son[y];
}
int main(){
	freopen("d.in","r",stdin);
	freopen("d.out","w",stdout);
	fin>>n;
	for(int i=0;i<=n;++i)base[i]=!i?1:Mul(base[i-1],2);
	for(int i=1,u,v;i<n;++i)fin>>u>>v,add(u,v),add(v,u);
	dfs1(1,0);dfs2(1,1);dfs3(1,0);val2[1]=val1[1];dfs4(1,0);
	fin>>q;
	for(int i=1,x,y;i<=q;++i){
		fin>>x>>y;
		if(dep[x]>dep[y])std::swap(x,y);
		int z=lca(x,y),w=getdist(x,y),ans;
		if(z!=x){
			ans=Mul(Mul(val1[x],val1[y]),base[w]);
			fout<<ans<<'\n';
			continue;
		}
		int to=find(y,x);
		ans=Mul(Mul(val1[y],Dec(val2[x],Mul(2,val1[to]))),base[w]);
		fout<<ans<<'\n';
	}
	return 0;
}

大眼鸹猫

发现始终满足单调不降的条件,这个限制没有用。把每一位拆开考虑,需要修改总量为 \(|a_i-b_i|\),修改次数一定时,我们希望 \(|a_i-b_i|\) 尽可能平均到每一次,而且修改次数越多肯定答案越小。初始我们把修改次数都设为 1,可以用一个堆来维护修改次数增大 1 时答案的减小量的最小值。

点击查看代码
#include<bits/stdc++.h>
#define mp std::make_pair
#define fi first
#define se second
using ll=long long;
using pii=std::pair<ll,int>;
const int P=998244353;
int n,m;
ll a[100005],b[100005],c[100005],d[100005],ans;
std::priority_queue<pii,std::vector<pii>> pq;
ll calc(ll tot,ll step){return (step-tot%step)*(tot/step)*(tot/step)+(tot%step)*(tot/step+1)*(tot/step+1);}
int main(){
	std::cin.tie(nullptr)->sync_with_stdio(false);
	freopen("attend.in","r",stdin);
	freopen("attend.out","w",stdout);
	std::cin>>n>>m;
	for(int i=1;i<=n;++i)std::cin>>a[i];
	for(int i=1;i<=n;++i)std::cin>>b[i];
	for(int i=1;i<=n;++i){
		if(a[i]==b[i])continue;
		c[i]=std::abs(a[i]-b[i]);
		ans+=c[i]*c[i]%P;
		d[i]=1;
		m--;
		if(d[i]<c[i]){
			pq.push(mp(calc(c[i],d[i])-calc(c[i],d[i]+1),i));	
		}
	}
	if(!m)std::cout<<0,exit(0);
	if(m<0)std::cout<<-1,exit(0);
	ans%=P;
	while(m--){
		if(pq.empty())break;
		auto now=pq.top();
		pq.pop();
		ans-=now.fi;
		if(d[now.se]<c[now.se]){
			d[now.se]++;
			pq.push(mp(calc(c[now.se],d[now.se])-calc(c[now.se],d[now.se]+1),now.se));	
		}
	}
	std::cout<<(ans%P+P)%P;
	return 0;
}

小猫吃火龙果

只有两种颜色的部分分,如果有 A 最后答案一定是 A,线段树维护区间翻转,区间查询个数即可。

考虑没有修改时,可以直接分块预处理出每种颜色最后到的结果。加上修改其实就是六种映射关系的置换,分块维护标记即可。散块重构还会带上 \(3\times 6\) 的常数,所以实际块长要取小一些。

点击查看代码
namespace Subtask3{
	static int L[1500],R[1500],tag[1500],bel[200005],f[6][1500][3],bsiz,bnum;
	static const int id[6][3]={{0,1,2},{0,2,1},{1,0,2},{1,2,0},{2,0,1},{2,1,0}};
	static const int map[3][6]={{2,3,0,1,5,4},{5,4,3,2,1,0},{1,0,4,5,2,3}};
	void build(){
		bsiz=160;bnum=n/bsiz;if(!bnum)bnum=1;
		for(int i=1;i<=bnum;++i)L[i]=R[i-1]+1,R[i]=bsiz*i;R[bnum]=n;
		for(int i=1;i<=bnum;++i)for(int j=L[i];j<=R[i];++j)bel[j]=i;
	}
	void update(int o){
		if(tag[o]){
			for(int i=L[o];i<=R[o];++i)s[i]=id[tag[o]][s[i]-'A']+'A';
			tag[o]=0;
		}
	}
	void rebuild(int o){
		for(int p=0;p<6;++p){
			for(int c=0;c<3;++c){
				int now=c;
				for(int j=L[o];j<=R[o];++j)now=w[now][id[p][s[j]-'A']]==1?now:id[p][s[j]-'A'];
				f[p][o][c]=now;
			}
		}
	}
	void modify(int l,int r,char x,char y){
		if(bel[l]==bel[r]){
			update(bel[l]);
			for(int i=l;i<=r;++i){
				if(s[i]==x)s[i]=y;
				else if(s[i]==y)s[i]=x;
			}
			rebuild(bel[l]);
			return;
		}
		update(bel[l]);
		for(int i=l;i<=R[bel[l]];++i){
			if(s[i]==x)s[i]=y;
			else if(s[i]==y)s[i]=x;
		}
		rebuild(bel[l]);
		int p=x-'A'+y-'A'-1;
		for(int i=bel[l]+1;i<bel[r];++i)tag[i]=map[p][tag[i]];
		update(bel[r]);
		for(int i=L[bel[r]];i<=r;++i){
			if(s[i]==x)s[i]=y;
			else if(s[i]==y)s[i]=x;
		}
		rebuild(bel[r]);
	}
	char query(int l,int r,int c){
		if(bel[l]==bel[r]){
			if(tag[bel[l]])update(bel[l]),rebuild(bel[l]);
			int now=c;
			for(int i=l;i<=r;++i)now=w[now][s[i]-'A']==1?now:s[i]-'A';	
			return now+'A';
		}
		int now=c;
		if(tag[bel[l]])update(bel[l]),rebuild(bel[l]);
		for(int i=l;i<=R[bel[l]];++i)now=w[now][s[i]-'A']==1?now:s[i]-'A';
		for(int i=bel[l]+1;i<bel[r];++i)now=f[tag[i]][i][now];
		if(tag[bel[r]])update(bel[r]),rebuild(bel[r]);
		for(int i=L[bel[r]];i<=r;++i)now=w[now][s[i]-'A']==1?now:s[i]-'A';
		return now+'A';
	}
	void main(){
		build();
		for(int i=1;i<=bnum;++i){
			for(int p=0;p<6;++p){
				for(int c=0;c<3;++c){
					int now=c;
					for(int j=L[i];j<=R[i];++j){
						now=w[now][id[p][s[j]-'A']]==1?now:id[p][s[j]-'A'];
					}
					f[p][i][c]=now;
				}
			}
		}
		for(int i=1;i<=m;++i){
			if(q[i].opt==0){
				if(q[i].x!=q[i].y)
				modify(q[i].l,q[i].r,q[i].x,q[i].y);
			}
			else{
				std::cout<<query(q[i].l,q[i].r,q[i].x-'A')<<'\n';
			}
		}
	}
}
posted @ 2023-11-12 17:15  Flandreqwq  阅读(58)  评论(1编辑  收藏  举报