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 记录。

这是 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\) 天,那么按照比例应该有两次部分分训练。)

我好菜。

posted @ 2026-07-24 18:23  嘎嘎喵  阅读(38)  评论(5)    收藏  举报