20260227 模拟测 总结

Preface

我为什么要浪费那么多时间在 T2 的假解上?换句话说,为什么我不能一开始就想到 T2 的正解???

我为什么要花费那么多时间在 T3 的调试上?换句话说,为什么我不能一开始就写对 T3 的细节???

时间分配问题。

我会 T4。


T1

估分 得分 挂分
\(100\) \(100\) \(0\)

个人评价:全场唯一水题。

注意到字典序的性质,最靠前的要分配最小的一定最优,于是我们用 map 随便乱搞就行了。

T2

估分 得分 挂分
\(100\) \(100\) \(0\)

我们要尽量使柴油不浪费,就先均分,均分后有剩下的柴油再让每台机器都 \(+1\)。不过显然无法把所有机器都 \(+1\),因为剩下的柴油是均分后的余数。

注意判断产出量上界 \(Q\) 以及机器启动下界 \(k\) 对计算过程的一些影响。

T3

估分 得分 挂分
\(100\) \(100\) \(0\)

怎么是,史山。

先把同种数字组成的序列所贡献的答案一把算掉。这可以用形似双指针的东西 \(O(n)\) 解决。

于是剩下的满足条件的,就一定是最大值 \(1\) 最小值 \(-1\)、或者最大值 \(2\) 最小值 \(-2\) 的区段了。

先考虑最大值 \(1\) 最小值 \(-1\)。为了使答案不重不漏,我们需要“有标志性地”去计算与求解,不妨让最小值 \(-1\) 贡献答案,且只让区段中最靠后的 \(-1\)(令其下标为 \(i\))贡献答案。

由于区段中必须存在 \(1\),直接考虑又非常麻烦,可以用容斥——具体地,我们用满足条件的区段中 \(i\) 左边有 \(1\) 的个数加上 \(i\) 右边有 \(1\) 的个数再减掉 \(i\) 左右两边都有 \(1\) 的个数即可求出 \(i\) 对答案的贡献。真绕……

分别求这三种类型的区段个数,只需要知道 \(i\) 前后离它最近的 \(1\) 和 \(-1\) 的位置,以及 \(i\) 前后能延伸的最长的绝对值都是 \(1\) 的区间长度即可。这些显然都可以在最开始时线性预处理得到。注意一些细节和边界的处理。

最大值 \(2\) 和最小值 \(-2\) 的同理,求解方式很类似,只是算区段个数时不需要考虑什么前后延伸绝对值相等区间什么什么的了,之所以上面那个需要考虑是因为一旦锁死最大值 \(1\) 最小值 \(-1\) 则区段中一定不能出现绝对值为 \(2\) 的数,但这里不同,\(2>1\) 且 \(-2<-1\),绝对值为 \(1\) 的数可以随意出现。

于是最后就做完了,只是细节比较多,写挂了有点难调。

我考场上发现大样例挂了之后使用和暴力解对拍的方式查出了好几个错。现在想来这也许就是当时最优的处理方案了……吧?

T4

估分 得分 挂分
\(50\) \(10\) \(40\)

死因:题目要求输出的是正整数但是我输出了一个浮点数,由于存在精度误差,一旦浮点数后面的小数位较多就会自动变成科学计数法,然后就挂没了。

唉唉,下次最后还是转成整数输出吧唉。

好了接下来讲正解。我必须说,这个题是真的不难。但是讲起来是真难。

以下令 \(dis\) 为题面中所述距离数组 \(a\),令 \(p\) 为题面中所述的距离数组平均数 \(x\)。

把 \(n \sum (dis_i - p)^2\) 拆了,可得价值为 \(n \sum ({dis_i} ^2 ) - ( \sum dis_i ) ^2\)。

考虑换根 DP。最初让 \(1\) 当根,也从 \(1\) 出发开始换根。

令 \(dp_u\) 为当 \(u\) 作为该树的根时的 \(\sum ({dis_i} ^2)\),\(f_u\) 为当 \(u\) 作为该树的根时的 \(\sum dis_i\)。有了这两个东西,就不难得出当 \(u\) 作为该树的根时整棵树的价值为 \(dp_u n - {f_u}^2\)。

显然 \(dp\) 和 \(f\) 的初值都是从根为 \(1\) 的情况那里得来的,可以跑一次 DFS 预处理 \(dis\) 数组。

接下来是转移。\(f\) 的转移是简单的:若要从 \(f_{fa}\) 转移到 \(f_{u}\)(其中 \(fa\) 是节点 \(u\) 的父节点),显然 \(u\) 子树内的节点所对应 \(dis\) 值统统 \(-1\) 而 \(u\) 子树外的节点所对应 \(dis\) 值统统 \(+1\)。预处理子树大小 \(sz\),不难得到转移方程 \(f_u = f_{fa} - sz_u + (n-sz_u)\)。

难点在于 \(dp\) 的转移。我们注意到普通的画图和推导已经对这个式子无能为力,只好开始推式子了。

为了方便推导与描述,我们令 \(dis'\) 数组为当 \(fa\) 为根时的 \(dis\) 数组经过重排后的结果,其中 \(dis'_1,dis'_2,\dots,dis'_{sz_u}\) 分别是 \(u\) 的子树上节点(也包括其自身)的 \(dis\) 值。

\[dp_u \]

\[= (dis'_1 - 1)^2 + \dots + (dis'_{sz_u} - 1)^2 + (dis'_{sz_u + 1} + 1)^2 + \dots + (dis'_n + 1)^2 \]

\[= ({dis'_1}^2 - 2 dis'_1 + 1) + \dots + ({dis'_{sz_u}}^2 - 2 dis'_{sz_u} + 1) + ({dis'_{sz_u + 1}}^2 + 2 dis'_{sz_u + 1} + 1) + \dots + ({dis'_n}^2 + 2 dis'_n + 1) \]

\[= ({dis'_1}^2 + {dis'_2}^2 + \dots + {dis'_n}^2) + n - 2 \sum_{i=1}^{sz_u} dis'_i + 2 \sum_{i=sz_u +1}^{n} dis'_i \]

\[= dp_{fa} + n - 2 \sum_{i=1}^{sz_u} dis'_i + 2 \sum_{i=sz_u +1}^{n} dis'_i \]

\[= dp_{fa} + n - 4 \sum_{i=1}^{sz_u} dis'_i + 2 \sum_{i=1}^{n} dis'_i \]

\[= dp_{fa} + 2 f_{fa} + n - 4 \sum_{i=1}^{sz_u} dis'_v \]

观察最后一个式子,我们发现,经过这样一番推导,最终只需要考虑所有在 \(u\) 的子树内的节点、在 \(fa\) 为根情况下到 \(fa\) 的距离之和!

考虑 \(sum_u\) 为 \(u\) 的每个后代节点到 \(u\) 的距离之和,对于叶子节点 \(t\) 有 \(sum_t = 0\),转移 \(sum_u = \sum_{v \in \text{son}(u)} sum_v + sz_u - 1\)。于是上面所需要的那个什么“所有在 \(u\) 的子树内的节点、在 \(fa\) 为根情况下到 \(fa\) 的距离之和”就是 \(sum_u + sz_u\) 了,因为 \(u\) 的子树内每个节点都要多走一步才能到 \(u\) 的父节点 \(fa\) 处。

整理一下,最终 \(dp\) 的转移式为:若要从 \(dp_{fa}\) 转移到 \(dp_{u}\)(其中 \(fa\) 是节点 \(u\) 的父节点),则有转移方程 \(dp_u = dp_{fa} + 2 f_{fa} + n - 4(sum_u + sz_u)\)。

于是总共也就需要跑两次 DFS:第一次预处理,算出 \(sz,sum\) 两个数组并求出当 \(1\) 为根时的 \(dis\) 以初始化 \(dp_1\) 和 \(f_1\);第二次换根 DP,按照转移方程

  • \(dp_u = dp_{fa} + 2 f_{fa} + n - 4(sum_u + sz_u)\);
  • \(f_u = f_{fa} - sz_u + (n-sz_u)\);

算出所有的 \(dp\) 和 \(f\)。

最终答案便是 \(\min_{i=1}^{n} dp_i n - {f_i}^2\)。

代码贼短。

是真的,不信你看。不好,看了你也不信。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 4e4+5;
LL T,n,dis[N],sz[N],sum[N];
LL dp[N],f[N],Ans;
vector<int> g[N];
LL read(){
    LL su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}
void DFS_mkss(int u,int fa){
    sz[u]=1,sum[u]=0,dis[u]=dis[fa]+1;
    for(int v:g[u])if(v^fa)
        DFS_mkss(v,u),sz[u]+=sz[v],sum[u]+=sum[v];
    sum[u]+=sz[u]-1;return;
}
void DFS_solrt(int u,int fa){
    if(u!=1)f[u]=f[fa]-sz[u]+(n-sz[u]),
        dp[u]=dp[fa]+2*f[fa]+n-4*(sum[u]+sz[u]);
    for(int v:g[u])if(v^fa)DFS_solrt(v,u);return;
}
int main(){
    T=read();
    while(T--){
        n=read();
        for(int i=1;i<=n;i++)
            g[i].clear(),dp[i]=0,f[i]=0;
        for(int i=1;i<n;i++){
            int x=read(),y=read();
            g[x].pb(y),g[y].pb(x);
        }dis[0]=-1;DFS_mkss(1,0);
        for(int i=1;i<=n;i++)
            dp[1]+=dis[i]*dis[i],f[1]+=dis[i];
        DFS_solrt(1,0);Ans=0x3f3f3f3f3f3f3f3f;
        for(int i=1;i<=n;i++)
            Ans=min(Ans,n*dp[i]-f[i]*f[i]);
        cout<<Ans<<"\n";
    }
    return 0;
}

Summary

总结一下这次模拟测四道题的情况。

T1:啥事没有。

T2:以后一定要确保解法正确,不要脑子里冒出一个解,不管三七二十一就开编,这样真的会浪费很多时间,而且特别搞人心态。

T3:不要追求速度,写代码的时候写一行思考一行,尤其是你明知细节很多的情况,如果还不仔细考虑各种边界情况的话……我只能说调试实在是太耗时了。

T4:真的只是因为没时间了,我们算算,五点左后考试刚结束我花了 \(1\) 分钟左右时间给求答案的式子变了个形,回到家思考约 \(10\) 分钟就得到了正解,编代码顶多 \(5\) 分钟,而且没犯错无需调试,所以得出结论只要考试时长延长 \(20\) 分钟我就能 AK。所以这题没做出来真的只能怪 T2 T3 用时太久了吧!!!

其实总结起来就一句话:想清楚再写。

行吧,那就记个教训吧。

posted @ 2026-02-27 20:31  嘎嘎喵  阅读(43)  评论(0)    收藏  举报