20260724 - 暑期部分分训练 1 总结
前言
省流:A 不可做,B 考见识。
Joler:我放 A 本来只是想让你们打个爆搜的,结果你们很多人都在死磕 A 浪费了 B 的时间……
:???那为什么不把 B 放前面,把 A 放后面?????顺序决定成败。
A - variance
赛时得分:\(8\)pts。
我写了一些奇奇妙妙的东西,获得了奇奇妙妙的分数。
我就讲讲暴力做法吧,首先你可以推出一个式子,这个要求输出的值就是 \(n \sum a_i^2 - (\sum a_i)^2\)。你用 vector 存一下 \(a\) 值,然后爆搜,每次枚举修改 \(2 \sim n-1\) 中的某个然后继续 DFS。要用 set 去重。
然后你就可以获得 \(24\)pts 的好成绩。
这里是 $24$pts 的代码
#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
#define _i128 __int128
using namespace std;
const int N = 1e4+5;
const LL INF = 0x3f3f3f3f3f3f3f3f;
LL n,a[N],Ans=INF;
set<vector<LL>> st;
int read(){
int 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;
}
LL sol(vector<LL> vec){
LL ps=0,sum=0;
for(LL x:vec)ps+=x*x,sum+=x;
return n*ps-sum*sum;
}
void DFS(vector<LL> now){
LL res=sol(now);
Ans=min(Ans,res);
st.isr(now);
for(int i=1;i+1<now.size();i++){
vector<LL> tmp=now;
tmp[i]=tmp[i-1]+tmp[i+1]-tmp[i];
if(st.count(tmp))continue;
DFS(tmp);
}return;
}
int main(){
n=read();
vector<LL> vec;
for(int i=1;i<=n;i++)
a[i]=read(),vec.pb(a[i]);
DFS(vec);
cout<<Ans<<"\n";
return 0;
}
接着,按照 Joler 老师的说法,加一些奇奇妙妙的剪枝,比如当 \(\frac{3}{4}res>Ans\) 时不继续考虑当前这个 \(res\) 的变化了,直接 return,这样就可以获得 \(32\)pts 的好成绩。
这里是 $32$pts 的代码
#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
#define _i128 __int128
using namespace std;
const int N = 1e4+5;
const LL INF = 0x3f3f3f3f3f3f3f3f;
LL n,a[N],Ans=INF;
set<vector<LL>> st;
int read(){
int 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;
}
LL sol(vector<LL> vec){
LL ps=0,sum=0;
for(LL x:vec)ps+=x*x,sum+=x;
return n*ps-sum*sum;
}
void DFS(vector<LL> now){
LL res=sol(now);
Ans=min(Ans,res);
st.isr(now);
if(res-res/4>Ans)return;
for(int i=1;i+1<now.size();i++){
vector<LL> tmp=now;
tmp[i]=tmp[i-1]+tmp[i+1]-tmp[i];
if(st.count(tmp))continue;
DFS(tmp);
}return;
}
int main(){
n=read();
vector<LL> vec;
for(int i=1;i<=n;i++)
a[i]=read(),vec.pb(a[i]);
DFS(vec);
cout<<Ans<<"\n";
return 0;
}
至于其他的做法,我也不会,老师也没讲,那就不管了。
好像能模拟退火做,以后什么时候闲的没事干来试试。
B - return
赛时得分:\(70\)pts(期望得分 \(75\)pts,挂分 \(5\)pts)。
挂分原因:某个 sb 把 > 打成 < 号了。
我觉得这道题很有趣啊,我会做一个部分分详解。接下来的过程中,我描述的顺序,是根据我想到(我会)该做法的顺序来讲解的,因此也不一定是正常比赛时应有的顺序。
第一档分:\(O(m \log m + n Q)\) 暴力
这个做法是我一看到这个题目就想出来的做法。
我们发现,题目就是让我们从指定点出发,沿着一部分(可能没有)海拔 \(> p\) 的边无代价走完后,再有代价(代价为路径长度)走到终点(\(1\) 号点),要求最小化总代价。
那么这个路径可以拆成两部分,就是前面海拔 \(>p\) 的部分和后面有代价走的部分。前面海拔 \(>p\) 的部分可以通过只加入海拔 \(>p\) 的边做 BFS(或者直接跑 DFS)之类代替,标记上能无代价走到的点;然后从这些可能无代价走到的点出发,求它们到 \(1\) 的总长度中最短的一个。
由于是无向图,后面有代价的部分可以通过从 \(1\) 出发的一次 Dijkstra 在全局搞定,因此我们要做的就是维护那些无代价能走到的点。
此时时间复杂度为 \(O(m \log m + m Q)\),如果 BFS/DFS 用并查集代替可以做到 \(O(m \log m + n Q)\)(并查集常数忽略不计)。并查集做法可以为后面的离线做法铺路,至于为什么,你看就行了。
第二档分:海拔只有一种
这个做法甚至在第一档分之后才被我发现,因为前面那个暴力太直觉性了,几乎我一看到这个题我就想到这个做法 emm……
海拔只有一种是很无脑的,因为只有两种情况,要么所有路都能通车,要么所有路都不能,炮哥 Dijkstra 预处理一下就行。
当然,某个 sb 在写这里输出的时候把 > 写成 < 了痛失 \(5\)pts,至于这个 sb 是谁我不说。
第三档分:离线做法
离线的话,我们就对第一档分做优化。第一档分的并查集做法非常妙啊,我们在离线做的时候,将所有询问按海拔高度从高到低排序,这样就会不断加进来一些边。然后再将边也按海拔从高到低排序,维护一个边序列中的指针,每次扫所有能加进来的边,塞进并查集,维护就行了。
显然也是需要打 Dijkstra 的。
插播:前三档分的代码
与赛时代码的编辑距离为 \(1\),至于是为什么,这只能怪某个 sb 手滑。
戳这里看前三档部分分的代码
#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
#define _i128 __int128
using namespace std;
const int N = 4e5+5;
const LL INF = 0x3f3f3f3f3f3f3f3f;
struct To{LL v,w,l;};
struct line{LL u,v,w,l;}ln[N];
LL T,n,m,Q,K,S;
vector<To> 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;
}
namespace sub1{
LL fa[N],val[N];
LL dis[N];
bool vis[N];
struct node{LL u,step;};
bool operator < (const node &A , const node &B){
return A.step>B.step;
}
priority_queue<node> q;
struct query{LL v,p,ans,id;}qy[N];
bool cmp_q(query q1,query q2){return q1.p>q2.p;}
bool cmp_ln(line l1,line l2){return l1.l>l2.l;}
bool cmp_id(query q1,query q2){return q1.id<q2.id;}
void Dijkstra(){
while(!q.empty())q.pop();
for(int i=1;i<=n;i++)dis[i]=INF,vis[i]=0;
dis[1]=0,q.push({1,0});
while(!q.empty()){
node tp=q.top();q.pop();
LL u=tp.u,ds=tp.step;
if(vis[u])continue;vis[u]=1;
for(int i=0;i<g[u].size();i++){
To tt=g[u][i];int v=tt.v,w=tt.w;
if(dis[v]>ds+w)dis[v]=ds+w,q.push({v,dis[v]});
}
}return;
}
int FF(int u){return (fa[u]==u?u:fa[u]=FF(fa[u]));}
void Merge(int u,int v){
int fu=FF(u),fv=FF(v);
if(fu!=fv)fa[fu]=fv,val[fv]=min(val[fv],val[fu]);
return;
}
void main(){
Dijkstra();
LL Ans=0;
while(Q--){
LL v=read(),p=read();
v=(v+K*Ans-1)%n+1,p=(p+K*Ans)%(S+1);
for(int i=1;i<=n;i++)fa[i]=i,val[i]=dis[i];
for(int i=1;i<=m;i++)
if(ln[i].l>p)Merge(ln[i].u,ln[i].v);
Ans=val[FF(v)];cout<<Ans<<"\n";
}
}//最万用的一种
void main2(){
Dijkstra();
LL Ans=0;
while(Q--){
LL v=read(),p=read();
v=(v+K*Ans-1)%n+1,p=(p+K*Ans)%(S+1);
if(p>=1)cout<<(Ans=dis[v])<<"\n";
else cout<<(Ans=0)<<"\n";
}
}//海拔为一种的预处理
void main3(){
Dijkstra();
for(int i=1;i<=Q;i++){
LL v=read(),p=read();
v=(v-1)%n+1,p=p%(S+1);
qy[i]={v,p,0,i};
}
sort(qy+1,qy+Q+1,cmp_q);
sort(ln+1,ln+m+1,cmp_ln);
int now=1;
for(int i=1;i<=n;i++)fa[i]=i,val[i]=dis[i];
for(int i=1;i<=Q;i++){
int v=qy[i].v,p=qy[i].p;
while(now<=m&&ln[now].l>p)
Merge(ln[now].u,ln[now].v),now++;
qy[i].ans=val[FF(v)];
}
sort(qy+1,qy+Q+1,cmp_id);
for(int i=1;i<=Q;i++)cout<<qy[i].ans<<"\n";
return;
}//离线做法
}
int main(){
//freopen("return5.in","r",stdin);
//freopen("return.out","w",stdout);
T=read();
while(T--){
n=read(),m=read();
for(int i=1;i<=n;i++)g[i].clear();
bool HB1=1;
for(int i=1;i<=m;i++){
LL u=read(),v=read();
LL w=read(),l=read();
g[u].pb({v,w,l}),g[v].pb({u,w,l});
ln[i]={u,v,w,l};
if(l!=1)HB1=0;
}
Q=read(),K=read(),S=read();
if(1ll*m*Q<=50000000)sub1::main();
else if(HB1)sub1::main2();
else if(!K)sub1::main3();
}
return 0;
}
正解:Kruskal 重构树
这也就是我省流里为啥说这题是“考见识”,说句不好听的,如果我会 Kruskal 重构树,我赛时有超过 \(80\%\) 的概率能切出来 /yun。
我们发现前面那个做法的瓶颈就是并查集那个部分,也就是怎么快速处理出来从指定节点出发,只经过 \(> p\) 的海拔的边的所有点的一个集合。当然,还需要求一个 \(\min dis\),不过这些都是比较好处理的。
然后我们发现不会 Kruskal 重构树,那赶紧来学一下,感觉这篇讲的挺明白的,至少我看 OI-wiki 没看懂看这篇一下子就入门成功了。
所以这个东西就有很强的性质啊!我们用最大生成树建重构树,每次询问,我们只需要找到树中最浅(深度最小)且海拔 \(>p\) 的节点,那么它的子树全部能从起点出发开车去到。这个可以由重构树是二叉堆推来。
所以你倍增搞搞,再维护个子树 \(\min\),就做完了。
正解代码
一定一定一定一定一定要注意的就是多测清空!!!!!!!!!!!!
这是 AC 代码。
#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
#define _i128 __int128
using namespace std;
const int N = 4e5+5;
const LL INF = 0x3f3f3f3f3f3f3f3f;
struct To{LL v,w,l;};
struct line{LL u,v,w,l;}ln[N];
struct node{LL u,step;};
bool operator < (const node &A , const node &B){
return A.step>B.step;
}
int T,n,m,cnt;//cnt 为重构树点数
int fa[N];//并查集
LL Q,K,S;//用于询问
LL val[N];//重构树中新点的点权
LL mn[N];//子树中 dis 权值的 min 值
int f[N][23];//用于倍增求 LCA
LL dis[N];bool vis[N];//用于 Dijkstra
vector<To> g[N];//存原始边
vector<int> e[N];//存重构树边
priority_queue<node> q;//用于 Dijkstra
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 Dijkstra(){
while(!q.empty())q.pop();
for(int i=1;i<=2*n;i++)dis[i]=INF,vis[i]=0;
dis[1]=0,q.push({1,0});
while(!q.empty()){
node tp=q.top();q.pop();
LL u=tp.u,ds=tp.step;
if(vis[u])continue;vis[u]=1;
for(auto [v,w,l]:g[u])
if(dis[v]>ds+w)dis[v]=ds+w,q.push({v,dis[v]});
}return;
}//Dijkstra 求最短路
int FF(int u){return (fa[u]==u?u:fa[u]=FF(fa[u]));}//并查集找根
bool cmp(line l1,line l2){return l1.l>l2.l;}//按海拔高度从高到低排序
void DFS_init(int u,int fa){
mn[u]=dis[u],f[u][0]=fa;
for(int i=0;f[u][i];i++)f[u][i+1]=f[f[u][i]][i];//预处理
for(int v:e[u]){
DFS_init(v,u);//递归子树
mn[u]=min(mn[u],mn[v]);//求 min
}return;
}
void Kruskal__(){
sort(ln+1,ln+m+1,cmp);//对边排序
for(int i=1;i<=n;i++)fa[i]=i;//初始化并查集
cnt=n;//新点编号从 n+1 开始
for(int i=1;i<=m;i++){
int fu=FF(ln[i].u),fv=FF(ln[i].v);
if(fu==fv)continue;//已经在同一集合中
val[++cnt]=ln[i].l;//点权为海拔高度
fa[fu]=cnt,fa[fv]=cnt,fa[cnt]=cnt;//合并集合
e[cnt].pb(fu),e[cnt].pb(fv);//从父节点向儿子节点连边
}DFS_init(cnt,0);return;//从根出发求路径 min 值
}
void Init__(){
for(int i=1;i<=n;i++)g[i].clear();
for(int i=1;i<=2*n;i++)e[i].clear();
for(int i=1;i<=2*n;i++)val[i]=0,mn[i]=0;
for(int i=1;i<=2*n;i++)
for(int j=0;j<=20;j++)f[i][j]=0;
cnt=0;return;//清空函数
}
int main(){
T=read();
while(T--){
n=read(),m=read();
Init__();//多测清空
for(int i=1;i<=m;i++){
LL u=read(),v=read(),w=read(),l=read();
ln[i]={u,v,w,l};
g[u].pb({v,w,l}),g[v].pb({u,w,l});
}Dijkstra();//预处理 1 到所有节点的距离
Kruskal__();//Kruskal 重构树
Q=read(),K=read(),S=read();
LL lstans=0;//在线算法
while(Q--){
LL x=read(),p=read();
x=(x-1+K*lstans)%n+1,p=(p+K*lstans)%(S+1);
for(int i=20;i>=0;i--)
if(f[x][i]&&val[f[x][i]]>p)x=f[x][i];//倍增一直往上跳
lstans=mn[x];cout<<lstans<<"\n";//输出
}
}
return 0;
}
总的来说还是一个挺好的题,也只能怨我没学过 Kruskal 重构树了,呵呵。
总结
没啥好总结的(。
A 我连纯暴力都没打我也是个神人,以及 A 我没发现是交换差分,如果发现了的话应该还会想到用随机化搞搞,也许就能获得 plc 那个分?但现在说起来都没意义了。
至于 B 除了挂的那 \(5\) 分其他我也不说什么,我确实不会 Kruskal 重构树这么个东西。
行呗。
哦对了,大家发现了没有,这是暑期部分分训练 \(1\),因此肯定还有 \(2\),甚至,你信不,我觉得会有 \(3\)。(推断缘由:这一次集训只有 \(5\) 天都有部分分训练,而下次集训有 \(10\) 天,那么按照比例应该有两次部分分训练。)
我好菜。

浙公网安备 33010602011771号