AHOI 2022 题目选做

真的是码农场,\(\tt T1\) 写两个小时,看到是道蓝题直接心态爆炸。

但是可以拿的分还是很多,如果早上没有这么困的话草上 \(300+\) 还是有希望的。

钥匙

题目描述

点此看题

解法

关注特殊性质 \(A\),发现可以得到若干个 \((x,y)\),表示依次经过 \(x,y\) 就会产生 \(1\) 的贡献。这个可以转化成 \(\tt dfn\) 序上的矩阵加,然后每个询问对应着一个单点查询,离线下来树状数组即可。

可以把上面的做法拓展,考虑贡献法。举个例子,对于路径 1122112,我们拆成三个点对的贡献:\((2,3),(1,4),(6,7)\);所以把它看作括号匹配是可以完美地处理贡献的,因为这样无论在前面或者后面添加什么东西,匹配点对都是不变的。也就是有贡献的匹配点对和询问没有关系,怎么样询问贡献的点对都是那些。

如何处理出匹配点对呢?利用 同一种的钥匙最多只有5把 的关键性质,我们把同种颜色的钥匙和宝箱建成虚树,然后以每个钥匙为起点 \(\tt dfs\),把钥匙看成 \(1\),宝箱看成 \(-1\),第一个权值为 \(0\) 的点就是这个钥匙匹配的宝箱,找到即可回溯。

时间复杂度 \(O(n\log n)\),看起来很长实际上随便打。

彩蛋

在实现这道题的时候,我和 \(rainybunny\) 发生了这样的对话:

rainybunny:卧槽,这题还要建双向边。

我:建双向边不是有手就行?

几分钟后.....

我:草,我虚树没有建双向边,改了他妈直接过样例。

#include <cstdio>
#include <vector>
#include <cassert>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 1000005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
void write(int x)
{
	if(x>=10) write(x/10);
	putchar(x%10+'0');
}
int n,m,k,a[M],b[M],fa[M][20],dfn[M],dep[M];
int t,rt,Ind,out[M],ans[M],X[M],Y[M];
vector<int> g[M],z[M],G[M];
struct node
{
	int x,l,r,f;
	bool operator < (const node &b) const
		{return x<b.x;}
}s[M*10],q[M];
void dfs(int u)
{
	dfn[u]=++k;X[u]=++Ind;
	dep[u]=dep[fa[u][0]]+1;
	for(int i=1;i<20;i++)
		fa[u][i]=fa[fa[u][i-1]][i-1];
	for(int v:g[u]) if(v^fa[u][0])
		fa[v][0]=u,dfs(v);
	out[u]=k;Y[u]=++Ind;
}
void ins(int lx,int rx,int ly,int ry)
{
	if(lx>rx || ly>ry) return ;
	s[++t]={lx,ly,ry,1};
	s[++t]={rx+1,ly,ry,-1};
}
void add(int x,int c)
{
	for(int i=x;i<=n;i+=i&(-i)) b[i]+=c;
}
int ask(int x)
{
	int r=0;
	for(int i=x;i>0;i-=i&(-i)) r+=b[i];
	return r;
}
int find(int u,int v)
{
	for(int i=19;i>=0;i--)
		if(dep[fa[u][i]]>dep[v])
			u=fa[u][i];
	return u;
}
void zxy(int u,int v)
{
	int lu=dfn[u],ru=out[u];
	int lv=dfn[v],rv=out[v];
	if(lu<=lv && lv<=ru)//u is a ancestor of v
	{
		int x=find(v,u);
		ins(1,dfn[x]-1,lv,rv);
		ins(out[x]+1,n,lv,rv);
	}
	else if(lv<=lu && lu<=rv)
	{
		int x=find(u,v);
		ins(lu,ru,1,dfn[x]-1);
		ins(lu,ru,out[x]+1,n);
	}
	else ins(lu,ru,lv,rv);
}
int cmp(int a,int b)
{
	int t1=a>0?X[a]:Y[-a];
	int t2=b>0?X[b]:Y[-b];
	return t1<t2;
}
int lca(int u,int v)
{
	if(dep[u]<dep[v]) swap(u,v);
	for(int i=19;i>=0;i--)
		if(dep[fa[u][i]]>=dep[v])
			u=fa[u][i];
	if(u==v) return u;
	for(int i=19;i>=0;i--)
		if(fa[u][i]^fa[v][i])
			u=fa[u][i],v=fa[v][i];
	return fa[u][0];
}
void get(int u,int fa,int d)
{
	if(b[u]==b[rt])
	{
		if(a[u]==1) d++;
		if(a[u]==2)
		{
			d--;
			if(d==0) zxy(rt,u);
			if(d<=0) return ;
		}
	}
	for(int v:G[u]) if(v^fa)
		get(v,u,d);
}
signed main()
{
	//freopen("keys.in","r",stdin);
	//freopen("keys.out","w",stdout);
	n=read();m=read();
	for(int i=1;i<=n;i++)
		a[i]=read(),b[i]=read();
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read();
		g[u].push_back(v);
		g[v].push_back(u);
	}
	dfs(1);
	for(int i=1;i<=n;i++)
		z[b[i]].push_back(i);
	for(int i=1;i<=n;i++) if(!z[i].empty())
	{
		static int A[M]={},vis[M]={},s[M]={};
		int k=0,k2=0,tp=0;
		for(int x:z[i]) A[++k]=x,vis[x]=1;
		sort(A+1,A+1+k,cmp);k2=k;
		for(int i=1;i<k2;i++)
		{
			int x=lca(A[i],A[i+1]);
			if(!vis[x]) vis[x]=1,A[++k]=x;
		}
		if(!vis[1]) A[++k]=1,vis[1]=1;k2=k;
		for(int i=1;i<=k2;i++) A[++k]=-A[i];
		sort(A+1,A+1+k,cmp);
		for(int i=1;i<=k;i++)
		{
			if(A[i]>0) s[++tp]=A[i];
			else
			{
				int t=s[tp--];
				if(t==1) break;
				G[s[tp]].push_back(t);
				G[t].push_back(s[tp]);
			}
		}
		for(int x:z[i]) if(a[x]==1)
			rt=x,get(x,0,0);
		for(int i=1;i<=k;i++) if(A[i]>0)
			vis[A[i]]=0,G[A[i]].clear();
	}
	for(int i=1;i<=m;i++)
	{
		int u=read(),v=read();
		q[i]=node{dfn[u],dfn[v],0,i};
	}
	sort(q+1,q+1+m);
	sort(s+1,s+1+t);
	for(int i=1;i<=n;i++) b[i]=0;
	for(int i=1,j=1;i<=m;i++)
	{
		while(j<=t && s[j].x<=q[i].x)
		{
			add(s[j].l,s[j].f);
			add(s[j].r+1,-s[j].f);
			j++;
		}
		ans[q[i].f]=ask(q[i].l);
	}
	for(int i=1;i<=m;i++)
		write(ans[i]),puts("");
}

山河重整

题目描述

点此看题

解法

考虑充要条件是:对于任意的前缀 \(i\)\([1,i]\) 内选取的数字和需要 \(\geq i\)

\(dp[i][j]\) 表示考虑了前 \(i\) 个数的选取情况,已经覆盖到了前缀 \(j\) 的方案数。转移考虑第 \(i\) 个数选不选取,如果选取则把 \(j\leftarrow j+i\),合法状态的要求是 \(j\geq i\),可以获得 \(60\) 分的高分。

优化可以考虑容斥,记 \(f[i]\) 表示第一次 \([1,i]\) 内选取的数字和等于 \(i\) 的方案数。那么不合法的状态就可以表示为:前 \(i\) 个数字和等于 \(i\),第 \(i+1\) 个数字强制不选取,后面的数字随意选取,那么答案是:

\[2^n-\sum_{i=0}^{n-1} f[i]\cdot 2^{n-i-1} \]

求出 \(f[i]\) 可以考虑第一次去重法(隶属于正难则反法),首先利用 \(O(n\sqrt n)\) 的拆分数 \(dp\) 求出不要求第一次,\([1,i]\) 内选取的数字和等于 \(i\) 的方案数,并且将他设置为 \(f[i]\) 的初始值。

然后从小到达去重 \(i\),现在考虑用 \(f[j]\) 去重 \(f[i]\),发现多算的部分是:强制不选取 \(j+1\)\([j+2,i]\) 之内选取数字和为 \(j-i\) 的方案数。关键的 \(\tt observation\) 是:可以用于去重的 \(j\) 满足 \(j\leq \frac{i}{2}\)

所以我们可以分治下去,每次考虑左半边对右半边的影响。可以统一地做一次 \(O(n\sqrt n)\) 的拆分数来去重。

时间复杂度 \(T(n)=O(n\sqrt n)+T(\frac{n}{2})=O(n\sqrt n)\),做拆分数的具体方法可以参考代码实现。

#include <cstdio>
const int M = 500005;
const int B = 1000;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,ans,f[M],g[M],b[M];
void add(int &x,int y) {if((x+=y)>=m) x-=m;}
void solve(int n)
{
	if(n<=1) return ;solve(n>>1);//递归左边
	for(int i=0;i<=n;i++) g[i]=0;
	for(int i=B;i;i--)//去重的过程仍然是拆分数
	{
		for(int j=n;j>=i;j--)
			g[j]=g[j-i];
		for(int j=0;j+i*(j+2)<=n;j++)
			add(g[j+i*(j+2)],f[j]);
		//这里的初始化变成了添加 i 个 j+2 的数字
		//因为要计算 [j+2,i] 内选数和为 j-i 的方案数 
		for(int j=i;j<=n;j++)
			add(g[j],g[j-i]);
	}
	for(int i=(n>>1)+1;i<=n;i++)
		add(f[i],m-g[i]);//正难则反
}
int main()
{
	n=read();m=read();
	for(int i=B;i;i--)
	//此时还在增加的有 i 个数,转移就是整体加 1
	{
		for(int j=n;j>=i;j--) f[j]=f[j-i];f[i]=1;
		//初始化,现在的 i 个数每个值都是 1
		for(int j=i;j<=n;j++) add(f[j],f[j-i]);
		//可以整体增加多次,所以做完全背包
	}
	f[0]=b[0]=1;solve(n);
	for(int i=1;i<=n;i++) b[i]=b[i-1]*2%m;
	for(int i=0;i<n;i++)
		add(ans,1ll*f[i]*b[n-i-1]%m);
	printf("%d\n",(b[n]+m-ans)%m);
}

回忆

题目描述

点此看题

解法

考虑调整法,初始每一条链单独走一遍,然后尽可能合并更多的链。

直接按照 \(\tt dfs\) 的顺序贪心,我们考虑现在面对的局面是怎么样的。对于子树 \(u\),有若干条链的终点是比 \(u\) 浅的,这些链暂时不能考虑,我们记在 \(s[u]\) 集合中;有若干条链的终点是在 \(u\) 子树中的,它们可以和子树外的路径自由匹配起来,我们记录它的个数 \(f[u]\);还有子树内的若干对匹配,每一对可以减少 \(1\) 的行走数量,记录其个数为 \(p[u]\)

考虑如何合并子树信息,对于 \(u\) 的所有儿子 \(v\)\(s\) 集合是可以直接启发式合并的(注意 \(s[v]\) 中终点深度恰好为 \(dep[u]\) 的链要取出来变成自由链)。

然后考虑尽可能把自由链给匹配起来,记 \(sum=\sum f_v\),那么只考虑自由链的匹配数是:

\[\min(\lfloor\frac{sum}{2}\rfloor,sum-\max f_v) \]

但是每一对匹配可以拆分成两个自由链,所以我们可能会把 \(p[v]\) 拆开以最大化匹配数量。

合并完子树信息之后,剩下的问题就是增加一条以 \(u\) 为起点的链,可以按照如下顺序考虑:

  • 如果 \(s[u]\) 非空,一定和最浅的一条链一并解决了。
  • 如果还存在自由边,可以把一条自由边和它合并,加入集合 \(s[u]\) 中。
  • 如果还存在匹配,把这个匹配拆分,和它合并之后形成一条自由边,另一条边加入 \(s[u]\) 中。
  • 否则直接加入 \(s[u]\) 中,增加初始答案 \(ans\)

那么最后的答案就是 \(ans-p[1]\),时间复杂度 \(O(n\log^2n)\)

#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
#include <set>
using namespace std;
const int M = 200005;
const int inf = 0x3f3f3f3f;
#define fi first
#define se second
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int T,n,m,ans,f[M],p[M],d[M],a[M];
vector<int> g[M];multiset<int> s[M];
void pre(int u,int fa)
{
	d[u]=d[fa]+1;
	for(int v:g[u]) if(v^fa) pre(v,u);
}
void dfs(int u,int fa)
{
	f[u]=p[u]=0;
	vector<pair<int,int>> b;
	for(int v:g[u]) if(v^fa)
	{
		dfs(v,u);
		while(!s[v].empty() && *s[v].rbegin()==d[u])
			f[v]++,s[v].erase(--s[v].end());
		if(s[u].size()<s[v].size()) swap(s[u],s[v]);
		for(int x:s[v]) s[u].insert(x);
		b.push_back({f[v],p[v]});
	}
	if(!b.empty())
	{
		int len=b.size(),sum=0;
		sort(b.begin(),b.end());
		for(int i=0;i+1<len;i++)
			sum+=b[i].fi+2*b[i].se;
		if(b[len-1].fi>=sum)
		{
			f[u]=b[len-1].fi-sum;
			p[u]=sum+b[len-1].se;
		}
		else
		{
			sum=0;
			for(int i=0;i+1<len;i++)
				sum+=b[i].fi,p[u]+=b[i].se;
			int d=max(0,(b[len-1].fi-sum+1)/2);
			p[u]-=d;sum+=2*d+b[len-1].fi;
			p[u]+=(sum>>1)+b[len-1].se;f[u]=sum&1;
		}
	}
	if(a[u]<inf)
	{
		if(!s[u].empty())
		{
			if(*s[u].begin()>a[u])
			{
				s[u].erase(s[u].begin());
				s[u].insert(a[u]);
			}
		}
		else if(f[u])
			f[u]--,s[u].insert(a[u]);
		else if(p[u])
			p[u]--,f[u]++,s[u].insert(a[u]);
		else
			ans++,s[u].insert(a[u]);
	}
}
void work()
{
	n=read();m=read();ans=0;
	for(int i=1;i<=n;i++)
		g[i].clear(),s[i].clear(),a[i]=inf;
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read();
		g[u].push_back(v);
		g[v].push_back(u);
	}
	pre(1,0);
	for(int i=1;i<=m;i++)
	{
		int u=read(),v=read();
		a[v]=min(a[v],d[u]);
	}
	dfs(1,0);
	printf("%d\n",ans-p[1]);
}
int main()
{
	T=read();
	while(T--) work();
}
posted @ 2022-06-06 16:57  C202044zxy  阅读(266)  评论(0编辑  收藏  举报