IOI2020集训队作业-10 (CF566C, CF700E, ARC092F)

这篇博客详细解析了IOI2020集训队的三道题目:CF566C Logistical Questions,CF700E Cool Slogans,ARC092F Two Faced Edges。针对每道题目,博主给出了解决方案和代码实现,涉及单峰函数、后缀自动机和强连通分量等算法知识。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

A - CF566C Logistical Questions

Sol

cost(u,v)=dis(u,v)32cost(u,v)=dis(u,v)^{3\over 2}cost(u,v)=dis(u,v)23dis(u,v)≥0dis(u,v)\ge 0dis(u,v)0的时候随dis(u,v)dis(u,v)dis(u,v)的递增而递增。所以,对于一条路径上的点u1,u2⋯uku_1,u_2\cdots u_ku1,u2uk和某一个固定的点ppph(x)=cost(ux,p)h(x) = cost(u_x,p)h(x)=cost(ux,p)是一个单峰函数。由于wi≥0w_i \ge 0wi0,所以g(x)=∑pcost(ux,p)⋅wpg(x) = \sum_{p} cost(u_x,p)\cdot w_pg(x)=pcost(ux,p)wp也是单峰函数。

故而对于一个点,与它相邻的节点中至多只有一个节点比它优秀。点分治即可。

如何快速找出更优的那个节点:假设点分治过程中当前的根节点为uuu,对uuu的某一个子树subxsub_xsubx内的所有点vvv,求出h(subx)=∑cost(u,v)wvh(sub_x) = \sum cost(u,v)w_vh(subx)=cost(u,v)wv这个函数的导数h′(subx)h'(sub_x)h(subx)。我们把决策点从uuu移往subxsub_xsubx这棵子树的时候,答案的变化量就是∑y!=xh′(suby)−h′(subx)\sum _{y!=x} h'(sub_y) - h'(sub_x)y!=xh(suby)h(subx)。取变化量最小的那棵子树就可以了。

Code

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
#define db long double
#define ll long long
using namespace std;
template <class T>
inline void rd(T &x) {
	x=0; char c=getchar(); int f=1;
	while(!isdigit(c)) { if(c=='-') f=-1; c=getchar(); }
	while(isdigit(c)) x=x*10-'0'+c,c=getchar(); x*=f;
}
const int N=2e5+10;
int head[N],ecnt,n;
struct ed { int to,next,w; }e[N<<1];
void ad(int x,int y,int w) {
	e[++ecnt]=(ed){y,head[x],w}; head[x]=ecnt;
	e[++ecnt]=(ed){x,head[y],w}; head[y]=ecnt;
}
int vis[N];
int dis[N],wi[N];
db sum[N],tot,ans=1e100;
int ansp;
void dfs(int u,int last) {
	sum[u]=0;
	for(int k=head[u];k;k=e[k].next) {
		int v=e[k].to; if(v==last) continue;
		dis[v]=dis[u]+e[k].w,dfs(v,u);
		sum[u]+=sum[v];
	}
	sum[u]+=wi[u]*sqrt(dis[u]);
	tot+=wi[u]*pow(dis[u],1.5);
}
int mi,siz,rt;
int sz[N];
void getrt(int u,int last) {
	sz[u]=1; int mx=0;
	for(int k=head[u];k;k=e[k].next) {
		int v=e[k].to; if(v==last||vis[v]) continue;
		getrt(v,u),sz[u]+=sz[v],mx=max(mx,sz[v]);
	}
	mx=max(mx,siz-sz[u]);
	if(mx<mi) mi=mx,rt=u;
}
void sol(int u) {
	vis[u]=1,tot=0;
	for(int k=head[u];k;k=e[k].next) {
		int v=e[k].to;
		dis[v]=e[k].w,dfs(v,u);
		sum[u]+=sum[v];
	}
	if(tot<ans) ans=tot,ansp=u;
	db _mi=1e100; int p=0;
	for(int k=head[u];k;k=e[k].next) {
		int v=e[k].to; if(vis[v]) continue;
		db tmp=(sum[u]-sum[v])-sum[v];
		if(tmp<_mi) _mi=tmp,p=v;
	}
	if(p) mi=1e9,siz=sz[p],getrt(p,u),sol(rt);
}
int main() {	
	rd(n);
	for(int i=1;i<=n;++i) rd(wi[i]);
	for(int i=1,x,y,w;i<n;++i) rd(x),rd(y),rd(w),ad(x,y,w);
	mi=1e9,siz=n,getrt(1,0),sol(rt);
	printf("%d %.10Lf",ansp,ans);
	return 0;
}

B - CF700E Cool Slogans

Sol

1.一定存在一种方案满足sis_isisi+1s_{i+1}si+1的最长border,不然si+1s_{i+1}si+1一定可以变得更短。显然这也意味着sis_isi会是si+1s_{i+1}si+1的后缀。
2.考虑子串在www中出现的位置的下标构成的集合,由于对于每个集合对应的字符串中最长的那个,这些字符串一定要么包含要么不交,所以“出现下标集合”相同的串,它们是等价的,也即是以它们作为序列的开头/结尾,能够得到的最长的序列是一样的。

所以可以直接用后缀自动机上的节点作为状态,能够转移到每个点的是它在failfailfail树上的祖先中、在它里面至少出现了两次的串。显然在failfailfail树上的深度越浅、dpdpdp值越小,所以直接找每个点能够转移到的深度最深的点就可以了。

判断是否可以从一个点转移到另一个点,可以用线段树合并维护每个节点的出现位置集合。

Code

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
#define PB push_back
#define ll long long
using namespace std;
template <class T>
inline void rd(T &x) {
	x=0; char c=getchar(); int f=1;
	while(!isdigit(c)){if(c=='-')f=-1; c=getchar();}
	while(isdigit(c)) x=x*10-'0'+c,c=getchar(); x*=f;
}
const int N=2e5+10;
int ANS;
int n;
int qv,ql,qr;
namespace tree {
	const int M=N*80;
	int ls[M],rs[M],sz[M],ncnt;
	inline int new_node() { return ++ncnt; }
	inline void push_up(int c) { sz[c]=sz[ls[c]]+sz[rs[c]]; }
	void insert(int l,int r,int &c) {
		c=new_node(); sz[c]=1;
		if(l==r) return;
		int mid=l+r>>1;
		if(qv<=mid) insert(l,mid,ls[c]);
		else insert(mid+1,r,rs[c]);
	}
	int un(int l,int r,int u,int v) {
		if(!sz[u]||!sz[v]) return u+v;
		int c=new_node();
		if(l==r) return sz[c]=1,c;
		int mid=l+r>>1;
		ls[c]=un(l,mid,ls[u],ls[v]),rs[c]=un(mid+1,r,rs[u],rs[v]);
		push_up(c);
		return c;
	}
	int query(int l,int r,int c) {
		if(!c) return 0;
		if(ql<=l&&qr>=r) return sz[c];
		int mid=l+r>>1,ans=0;
		if(ql<=mid) ans+=query(l,mid,ls[c]);
		if(qr>mid) ans+=query(mid+1,r,rs[c]);
		return ans;
	}
}

namespace SAM {
	const int M=N<<1;
	int ch[M][26],len[M],fail[M],rt[M],pos[M];
	int ncnt=1,last=1;
	void insert(int c,int _pos) {
		int cur=++ncnt,p=fail[last]; ch[last][c]=cur,len[cur]=len[last]+1; last=cur;
		qv=pos[cur]=_pos,tree::insert(1,n,rt[cur]);
		for(;p&&!ch[p][c];p=fail[p]) ch[p][c]=cur;
		if(!p) return (void)(fail[cur]=1);
		int q=ch[p][c];
		if(len[q]==len[p]+1) return (void)(fail[cur]=q);
		int cl=++ncnt;
		for(int i=0;i<26;++i) ch[cl][i]=ch[q][i];
		len[cl]=len[p]+1;
		fail[cl]=fail[q],fail[q]=fail[cur]=cl;
		for(;ch[p][c]==q;p=fail[p]) ch[p][c]=cl;
	}
	int f[M];
	vector<int> son[M];
	int que[N],hd,tl;
	bool jud(int u,int v) {
		ql=pos[v]-len[v]+len[u],qr=pos[v]-1;
		if(ql>qr) return 0;
		return tree::query(1,n,rt[u]);
	}
	void dfs(int u) {
		int prehd=hd;
		for(int j=19;j>=0;--j)
			if((1<<j)+hd<=tl&&jud(que[hd+(1<<j)],u)) hd+=(1<<j);
		if(hd<=tl) f[u]=f[que[hd]]+1;
		ANS=max(ANS,f[u]);
		que[++tl]=u;
		for(int i=0;i<son[u].size();++i) dfs(son[u][i]);
		--tl;
		hd=prehd;
	}
	int buc[N],id[M];
	void sol() {
		for(int i=1;i<=ncnt;++i) buc[len[i]]++;
		for(int i=1;i<=n;++i) buc[i]+=buc[i-1];
		for(int i=1;i<=ncnt;++i) id[buc[len[i]]--]=i;
		
		for(int i=ncnt;i>=1;--i) {
			int u=id[i];
			if(fail[u]) {
				rt[fail[u]]=tree::un(1,n,rt[u],rt[fail[u]]);
				son[fail[u]].PB(u);
				if(!pos[fail[u]]) pos[fail[u]]=pos[u];
			}
		}
		hd=1;
		dfs(1);
	}
}
char str[N];
int main() {
	rd(n);
	scanf("%s",str+1);
	for(int i=1;i<=n;++i) SAM::insert(str[i]-'a',i);
	SAM::sol();
	printf("%d",ANS);
	return 0;
}

C - ARC092F Two Faced Edges

Sol

假设修改的边是u→vu\to vuv,判断强连通分量的数量是否改变,等价于判断(uuuvvv是否属于同一个强连通分量)是否有变化。

反向前,(uuuvvv是否属于同一个强连通分量)=(是否存在从vvvuuu的路径)。

反向后,(uuuvvv是否属于同一个强连通分量)=(原图中是否存在从uuuvvv且不经过u→vu\to vuv的路径)。

对于每个点对解决这个问题就可以了。

对于第二个问题,可以枚举uuu,然后依次枚举vvv,对于剩下的点记录至多两个能够在**不经过uuu**的情况下走到它的vvv。因为不允许经过uuu,所以只有第一步会走从uuu出发的边。

Code

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <bitset>
#include <vector>
#include <queue>
#define PB push_back
#define PII pair<int,int>
#define MP make_pair
#define fir first
#define sec second
#define ll long long
using namespace std;
template <class T>
inline void rd(T &x) {
	x=0; char c=getchar(); int f=1;
	while(!isdigit(c)){if(c=='-')f=-1; c=getchar();}
	while(isdigit(c)) x=x*10-'0'+c,c=getchar(); x*=f;
}
const int N=1010,M=1003;
bitset<M> G[N],A[N],B;
int n,m;
int vis[N][2];
int inq[N];
queue<int> que;
void push(int u,int t) {
	if(!vis[u][0]) vis[u][0]=t;
	else if(!vis[u][1]&&vis[u][0]!=t) vis[u][1]=t,B[u]=0;
	else return;
	A[t][u]=0;
	if(!inq[u]) que.push(u),inq[u]=1;
}
int mp[N][N],p1[N][N],p2[N][N];
void sol(int s) {
	B[s]=0;
	for(int i=1;i<=n;++i) if(mp[s][i]) push(i,i);
	while(!que.empty()) {
		int u=que.front(); que.pop(),inq[u]=0;
		while(1) {
			int v=((A[vis[u][0]]|A[vis[u][1]])&G[u]&B)._Find_first();
			if(v==M) break;
			if(vis[u][0]) push(v,vis[u][0]);
          	if(vis[u][1]) push(v,vis[u][1]);
		}
	}
	for(int i=1;i<=n;++i) if(s!=i) {
		p1[s][i]=vis[i][0]||vis[i][1]||mp[s][i];
		p2[s][i]=(vis[i][0]&&vis[i][0]!=i)||(vis[i][1]&&vis[i][1]!=i);
	}
	for(int i=1;i<=n;++i) {
		if(vis[i][0]) A[vis[i][0]][i]=1,vis[i][0]=0;
		if(vis[i][1]) A[vis[i][1]][i]=1,vis[i][1]=0;
		B[i]=1;
	}
}
vector<PII> E;
int main() {
	rd(n),rd(m);
	for(int i=1,x,y;i<=m;++i) rd(x),rd(y),G[x][y]=mp[x][y]=1,E.PB(MP(x,y));
	for(int i=1;i<=n;++i) for(int j=1;j<=n;++j) A[i][j]=1;
	for(int i=1;i<=n;++i) B[i]=1;
	for(int i=1;i<=n;++i) sol(i);
	for(int i=0;i<E.size();++i) { 
		int x=E[i].fir,y=E[i].sec,flg;
		if(p1[y][x]) flg=(p2[x][y]==1);
		else flg=(p2[x][y]==0);
		if(flg) printf("same\n");
		else printf("diff\n");
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值