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\) 个数字强制不选取,后面的数字随意选取,那么答案是:
求出 \(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\),那么只考虑自由链的匹配数是:
但是每一对匹配可以拆分成两个自由链,所以我们可能会把 \(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();
}