10.26模拟赛
预感最近的模拟赛都是CF重组场。
T1
考场思路:
纯暴力模拟。
先求出所有点到\(0\)号点的路径上经过的节点。
然后再转化为两个点之间的路径(0号点到左端点和0号点到左端点减去0号点到lca),记录哪些点在路径上,对于每个路径求出mex。
最后统计mex的贡献。
瓶颈在于求解mex需要依赖于求出具体每条路径经过了哪些节点。
时间复杂度应该在\(O(n^2logn)\)左右,应该可以拿30分,但是实际上挂到了5分。
正解思路:
一条路径的mex为\(k\)的充要条件为:\([0,k-1]\)在这条路径上都出现了,但是\(k\)没有出现。
维护\(k\)在一条路径上没有出现是不好维护的,考虑容斥出来用\(mex\ge k\)的路径条数,减去\(mex\ge k+1\)的路径条数,即为\(mex\)为\(k\)的路径条数。
现在问题转化为了求\(mex\)为\([0,n-1]\)的路径条数。
考虑依次枚举每个点。
如果当前枚举的点是\(k\),\([0,k-1]\)都在一条链上,链上的端点为\(st\)和\(ed\)。
如果\(k\)也在这条链上,那么它有可能成为链的端点。
如果\(st\)和\(ed\)的lca不为其中一个,说明此时链不被算在st或ed的子树中(类似一下这种情况)。那么路径条数为\(siz[st]\times siz[ed]\)。

从st的子树(不包含当前链)中的任一节点到ed的子树(不包含当前链)中的任一节点都经过了当前链,这些路径的mex一定大于等于\(k\)。
如果\(st\)和\(ed\)的lca为其中一个,说明此时链被算在st或ed的其中一个子树中(类似于以下这种情况)。

此时的路径条数为\(siz[st]\times(n-siz[ed'])\)
如果\(k\)不在这条链上,那么\([k,n]\)之间的\(mex\)一定为\([0,k-1]\)之间的数,而\(mex\)为\([0,k-1]\)的路径数已经统计过了,此时就可以跳出了。
code
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long LL;
int const maxe=400101;
int const maxn=200101;
int const Log=19;
int t,n;
struct Edge{
int nxt,to;
}e[maxe];
int head[maxn],tot;
int dep[maxn],f[maxn][Log+1];
int siz[maxn];
void addedge(int x,int y){
e[++tot].nxt=head[x];
head[x]=tot;
e[tot].to=y;
}
void dfs(int x,int fa){
dep[x]=dep[fa]+1;siz[x]=1;
f[x][0]=fa;
for(int k=1;k<=Log;k++)
f[x][k]=f[f[x][k-1]][k-1];
for(int i=head[x];i;i=e[i].nxt){
int y=e[i].to;
if(y==fa) continue;
dfs(y,x);
siz[x]+=siz[y];
}
}
int get_lca(int x,int y){
if(dep[x]>dep[y]) swap(x,y);
for(int i=Log;i>=0;i--)
if(dep[f[y][i]]>=dep[x])
y=f[y][i];
if(x==y) return x;
for(int i=Log;i>=0;i--)
if(f[x][i]!=f[y][i])
x=f[x][i],y=f[y][i];
return f[x][0];
}
int dist(int x,int y){
int lca=get_lca(x,y);
return dep[x]+dep[y]-2*dep[lca];
}
LL ans[maxn];
int main(){
scanf("%d",&t);
while(t--){
memset(head,0,sizeof(head));tot=0;
memset(ans,0ll,sizeof(ans));
scanf("%d",&n);
for(int i=1;i<=n-1;i++){
int x,y;
scanf("%d%d",&x,&y);
addedge(x,y);addedge(y,x);
}
dfs(0,0);
ans[0]=1ll*(n)*(n-1);ans[0]>>=1ll;
for(int i=head[0];i;i=e[i].nxt){
int y=e[i].to;
ans[1]+=1ll*siz[y]*(n-siz[y]);
}
ans[1]+=1ll*(siz[0]-1)*(n-siz[0]+1);ans[1]>>=1ll;
int st=0,ed=0;
for(int i=1;i<n;i++){
int len1=dist(st,ed),len2=dist(st,i)+dist(i,ed);
if(len1!=len2){
if(len2-len1==2*dist(i,st)) st=i;
else if(len2-len1==2*dist(i,ed)) ed=i;
else break;
}
int lca=get_lca(st,ed);
if((st!=lca)&&(lca!=ed)) ans[i+1]=1ll*siz[st]*siz[ed];
else{
int x=st,y=ed;
if(dep[x]<dep[y])swap(x,y);
ans[i+1]=1ll*siz[x];
for(int k=Log;k>=0;k--)
if(dep[f[x][k]]>dep[y])
x=f[x][k];
ans[i+1]*=1ll*(n-siz[x]);
}
}
for(int i=0;i<n;i++) ans[i]-=ans[i+1];
for(int i=0;i<=n;i++)
printf("%lld ",ans[i]);
puts("");
}
return 0;
}
T2
考场思路:
看见完全平方数就奔着数学去了…最后想到了DP,但其实我看出来了也不会写转移嘿……最后\(k=0\)的简单贪心分也没拿到QwQ。
正解思路:
其实是个DP。
DP方程需要啥设啥。
状态表示:\(dp[i][j]\)表示当前划分到第\(i\)个数,做了\(j\)次修改后的最少划分段数。
是个区间划分问题,最基础的转移是枚举最后一段的起点进行转移,时间复杂度至少是\(O(n^2)\)级别的。
考虑优化。
观察到当修改次数为定值时,最少划分段数一定是与划分的数成正相关的。也就是说当dp方程的第二维为定值时,dp方程第一维增长,dp方程的值一定是一个单调不下降序列。
基于这样的性质,只要找到尽可能靠前的合法位置进行转移。
dp方程在第二维固定,第一维增长时,方程的值是个定值。尽可能靠前一定是最优的,方程的值不变。
可以用尺取法预处理出来一个\(l\)数组,其中\(l[i][j]\)表示最小的\(pos\)使得\([pos,i]\)作为一个整段消耗了\(j\)次修改。
code
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
int const maxn=200101;
int const maxa=10000101;
int const maxk=23;
int t;
int n,k;
int a[maxn];
int vis[maxa];
int l[maxn][maxk],f[maxn][maxk];
int main(){
scanf("%d",&t);
while(t--){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
int tmp=a[i];a[i]=1;
for(int j=2;j*j<=tmp;j++){
if(tmp%j==0){
int cnt=0;
while(tmp%j==0) tmp/=j,++cnt;
if(cnt&1) a[i]*=j;
}
}
if(tmp>1) a[i]*=tmp;
}
for(int lim=0;lim<=k;lim++){
int cnt=0;
for(int i=1,j=1;i<=n;i++){
vis[a[i]]++;
if(vis[a[i]]>=2) ++cnt;
if(cnt>lim){
while(cnt>lim){
if(vis[a[j]]>=2) --cnt;
vis[a[j]]--;
j++;
}
}
l[i][lim]=j;
}
for(int i=1;i<=n;i++)
vis[a[i]]=0;
}
f[0][0]=0;
for(int i=1;i<=n;i++){
for(int j=0;j<=k;j++){
for(int x=0;x<=j;x++)
f[i][j]=min(f[i][j],f[l[i][x]-1][j-x]+1);
}
}
int ans=0x7f7f7f7f;
for(int i=0;i<=k;i++)
ans=min(ans,f[n][i]);
printf("%d\n",ans);
}
return 0;
}

浙公网安备 33010602011771号