UOJ Round #2 题解

A

由于并不需要使操作次数尽可能少,所以还是很容易构造的。

直接将每个左括号放到前 n n n 位,然后剩下右括号就在后 n n n 位,这就合法了。

代码如下:

#include <cstdio>
#include <cstring>

int n,now=0;
char s[200010];

int main()
{
	scanf("%s",s+1); n=strlen(s+1);
	printf("%d\n",n/2);
	for(int i=1;i<=n;i++)
	if(s[i]=='(')printf("%d %d\n",++now,i);
}

B

找负环理所当然地要请出Bellman-Ford算法,由于边上还有一个系数,所以数组设成三维: f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k] 表示从 1 1 1 出发至多走 i i i 步到 j j j 并且系数为 k k k 的最短路,那么从 1 1 1 出发至多走 i i i 步到 j j j 的最短路长度就是 min ⁡ { f [ i ] [ j ] [ k ] + k x } \min\{f[i][j][k]+kx\} min{f[i][j][k]+kx}

那么可以得到出现负环的要求:存在某个点满足
min ⁡ { f [ n ] [ i ] [ k ] + k x } < min ⁡ { f [ n − 1 ] [ i ] [ j ] + j x } \min\{f[n][i][k]+kx\}<\min\{f[n-1][i][j]+jx\} min{f[n][i][k]+kx}<min{f[n1][i][j]+jx}

由于我们需要的是不存在负环,所以不等式变为:
min ⁡ { f [ n ] [ i ] [ k ] + k x } ≥ min ⁡ { f [ n − 1 ] [ i ] [ j ] + j x } \min\{f[n][i][k]+kx\}\geq\min\{f[n-1][i][j]+jx\} min{f[n][i][k]+kx}min{f[n1][i][j]+jx}

下面对于每个 i i i 求出这个不等式的解集即可。

首先枚举 k k k,则变为求这个不等式的解集:
f [ n ] [ i ] [ k ] + k x ≥ min ⁡ { f [ n − 1 ] [ i ] [ j ] + j x } f[n][i][k]+kx \geq\min\{f[n-1][i][j]+jx\} f[n][i][k]+kxmin{f[n1][i][j]+jx}

因为 f [ n ] [ i ] [ k ] + k x f[n][i][k]+kx f[n][i][k]+kx 要大于右边的最小值,也就是说,大于其中任意一个即可,于是我们再枚举 j j j 求出每个不等式的解集,然后求个并集即可。

再看回一开始的不等式, min ⁡ { f [ n ] [ i ] [ k ] + k x } ≥ min ⁡ { f [ n − 1 ] [ i ] [ j ] + j x } \min\{f[n][i][k]+kx\}\geq\min\{f[n-1][i][j]+jx\} min{f[n][i][k]+kx}min{f[n1][i][j]+jx},左边的最小值要大于右边的最小值,也就是说,对于每一个 k k k,左边的柿子都要大于右边的最小值,所以我们将上面求出来的并集们再求个交集就得到解集了。

最后求解时,因为如果一个点在负环内,这个点能到达的所有点都会受到影响,所以对于每个 i i i,求解时还需要将能到达 i i i 的所有点的解集求个交集,才能得到最后的答案。

说的简单,事实上,你会发现网上的大佬们都用到了补集。回顾上面的流程:

  1. 枚举 k k k
  2. 枚举 j j j
  3. f [ n ] [ i ] [ k ] + k x ≥   f [ n − 1 ] [ i ] [ j ] + j x f[n][i][k]+kx \geq\ f[n-1][i][j]+jx f[n][i][k]+kx f[n1][i][j]+jx 的解集
  4. 3 3 3 中的解集求并集
  5. 4 4 4 中的解集求交集
  6. 求解:将能到达 i i i 的所有点的解集求交集

我们发现, 3 3 3 中求出来的解集都形如 x ≤ p x\leq p xp x ≥ p x\geq p xp,求并集之后就形如 ( − ∞ , t ) ∪ ( t ′ , ∞ ) (-\infty,t)\cup(t',\infty) (,t)(t,),然后发现,第 5 5 5 步很难做,更别说第 6 6 6 步了。

于是这时候用到了补集,我们将第 4 4 4 步中求出来的并集求个补集,变成 [ t , t ′ ] [t,t'] [t,t],然后都存起来,跳过第 5 5 5 步,直接到第 6 6 6 步,将能到达 i i i 的所有点的解集的补集们聚在一起,然后求出他们的并集的补集即可,具体操作就见代码了。

补充一句:其实代码中在第 3 3 3 步就求补集了qwq,并没有等到第 4 4 4 步(因为都是借鉴网上大佬的嘛),求了补集之后第 4 4 4 步就应该求交集而不是并集了。

代码如下:

#include <cstdio>
#include <cstring>
#include <cmath>
#include <vector>
#include <algorithm>
using namespace std;
#define maxn 110
#define f(i,j,k) f[i][j][k+105]
#define inf 999999999999999999
#define ll long long

int n,m,len;
struct edge{int x,y,z,k;};
edge e[maxn*maxn];
void buildroad(int x,int y,int z,int k){e[++len]=(edge){x,y,z,k};}
ll f[maxn][maxn][maxn<<1];//注意,第三维可能是负数,所以要将第三维整体向右挪
int g[maxn][maxn];//用来判断某两个点之间能否到达
void floyd()
{
	for(int i=1;i<=n;i++)g[i][i]=1;
	for(int k=1;k<=n;k++)for(int i=1;i<=n;i++)
	for(int j=1;j<=n;j++)g[i][j]|=g[i][k]&g[k][j];
}
void bellman_ford()
{
	for(int i=0;i<=n;i++)
	for(int j=1;j<=n;j++)
	for(int k=-n;k<=n;k++)f(i,j,k)=inf;
	f(0,1,0)=0;
	for(int i=1;i<=n;i++)
	{
		memcpy(f[i],f[i-1],sizeof(f[i]));
		for(int j=1;j<=m;j++)
		for(int k=-n;k<=n;k++)
		if(f(i-1,e[j].x,k)<inf)
		f(i,e[j].y,k+e[j].k)=min(f(i,e[j].y,k+e[j].k),f(i-1,e[j].x,k)+e[j].z);
	}
}
struct par{//pair用不习惯qwq,还是自己写的好
	ll x,y;
	par(ll xx,ll yy):x(xx),y(yy){}
	bool operator <(const par &b)const{return x==b.x?(y<b.y):(x<b.x);}
};
vector<par> a[maxn],s;
void solve()
{
	for(int i=1;i<=n;i++)
	{
		for(int k=-n;k<=n;k++)
		if(f(n,i,k)<inf)
		{
			ll l=-inf,r=inf;
			for(int j=-n;j<=n;j++)
			if(f(n-1,i,j)<inf)
			{
				if(k>j)r=min(r,(ll)ceil(1.0*(f(n-1,i,j)-f(n,i,k))/(k-j)));
				else if(k<j)l=max(l,(ll)floor(1.0*(f(n-1,i,j)-f(n,i,k))/(k-j)));
				else if(f(n,i,k)>=f(n-1,i,j)){l=inf,r=-inf;break;}
				//k=j意味着方程中x的系数为0,如果此时柿子仍然成立,那么说明对于当前k
				//找到了一个j,满足无论x是多少柿子都成立,所以不用再继续往下找了,解集的补集必然为空集
			}
			if(l<r)a[i].push_back(par(l,r));
			//如果l>=r,那么这个补集就是空的,对后面求并集没有任何用
		}
	}
}
void getans()
{
	for(int i=1;i<=n;i++)
	{
		ll l=inf,r=-inf,maxr=-inf;s.clear();
		//maxr记录目前最靠右的右端点
		for(int j=1;j<=n;j++)if(g[1][j]&&g[j][i])
		for(int k=0;k<a[j].size();k++)s.push_back(a[j][k]);//收集所有能到i的点的补集们
		sort(s.begin(),s.end());//以左端点为第一关键字,右端点为第二关键字,从小到大排
		for(int j=0;j<s.size();j++)
		{
			//很显然最后的解集只有一段,不可能形如[l,r]∪[l',r'],所以找到一段解集就可以break了
			if(j==0&&s[j].x>-inf){l=-inf,r=s[j].x;break;}
			//假如(-∞,s[0].x)没有被任何补集覆盖,那么这就是答案了
			if(maxr!=-inf&&maxr<=s[j].x){l=maxr,r=s[j].x;break;}
			//判断中间是否空出一段没被覆盖
			maxr=max(maxr,s[j].y);
		}
		if(r==-inf&&maxr<inf)l=maxr,r=inf;
		if(l==-inf||r==inf||!s.size())printf("-1\n");
		else printf("%lld\n",max(r-l+1,0ll));
	}
}

int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1,x,y,z,k;i<=m;i++)
	scanf("%d %d %d %d",&x,&y,&z,&k),buildroad(x,y,z,k),g[x][y]=1;
	floyd();bellman_ford();
	solve();getans();
}

C

先转化成求 f ( i ) = ∑ u , v [ i ∣ f ( u , v ) ] f(i)=\sum_{u,v}[i|f(u,v)] f(i)=u,v[if(u,v)],即求出有多少个 f ( u , v ) f(u,v) f(u,v) i i i 的倍数,然后最后搞一搞就可以求出有多少个 f ( u , v ) f(u,v) f(u,v) 恰好为 i i i

考虑根号分治,对于 d > n d>\sqrt n d>n ,设 h i , j h_{i,j} hi,j 表示 i i i 子树内深度为 j j j 的点数( i i i 的深度为 0 0 0),每次启发式合并子树的 h h h,发现当自己之前的子树中最大深度大于 n \sqrt n n 且子树的最大深度大于 n \sqrt n n 时,深度的 gcd ⁡ \gcd gcd 才可能 > n >\sqrt n >n ,此时暴力枚举每个 d > n d>\sqrt n d>n 更新 f f f 即可。由于都大于 n \sqrt n n 的合并次数不可能超过 n \sqrt n n 次,所以时间复杂度为 n n log ⁡ n n\sqrt n\log n nn logn

而对于 d ≤ n d\leq \sqrt n dn ,这样的 d d d 不超过 n \sqrt n n 个,直接 d d d O ( n ) O(n) O(n) dp \text{dp} dp 对于每个 d d d 求解即可,具体参考代码:

#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
#define maxn 200010
#define ll long long

int n,fa[maxn],dep[maxn];
ll f[maxn],dirf[maxn];
const int block=150;
void solve1(){
	static int g1[maxn],g2[maxn],upto[maxn];
	for(int i=1;i<=n;i++)upto[i]=i;
	for(int d=1;d<=min(block,n);d++){
		memset(g1,0,sizeof(g1));
		memset(g2,0,sizeof(g2));
		for(int i=n;i>1;i--){
			g1[i]++;
			if(upto[i]!=1){
				g2[upto[i]]+=g1[i];
				upto[i]=fa[upto[i]];
			}
			f[d]+=1ll*g1[fa[i]]*g2[i];
			g1[fa[i]]+=g2[i];
		}
	}
}
void solve2(){
	static vector<int> h[maxn];
	for(int i=n;i>=1;i--){
		h[i].push_back(1);
		if(h[fa[i]].size()<h[i].size())h[fa[i]].swap(h[i]);
		int *a=h[fa[i]].data(),a_n=h[fa[i]].size();
		int *b=h[i].data(),b_n=h[i].size();
		if(a_n>block&&b_n>block){
			for(int d=block+1,lim=min(a_n,b_n);d<=lim;d++){
				int tot1=0,tot2=0;
				for(int i=d;i<=a_n;i+=d)tot1+=a[a_n-i];
				for(int i=d;i<=b_n;i+=d)tot2+=b[b_n-i];
				f[d]+=1ll*tot1*tot2;
			}
		}
		for(int i=1;i<=b_n;i++)a[a_n-i]+=b[b_n-i];
	}
}

int main()
{
	scanf("%d",&n);
	for(int i=2;i<=n;i++)scanf("%d",&fa[i]);
	for(int i=2;i<=n;i++)dep[i]=dep[fa[i]]+1,dirf[dep[i]]++;
	for(int i=n-1;i>=1;i--)dirf[i]+=dirf[i+1];
	solve1();solve2();
	for(int i=n;i>=1;i--)for(int j=i+i;j<=n;j+=i)f[i]-=f[j];
	for(int i=1;i<n;i++)printf("%lld\n",f[i]+dirf[i]);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值