2026gdsy秋季集训(10月)

10.7

T1

手玩一下就能发现,答案自由度其实挺高的,设a是初速度,b是加速度,c是答案,式子形如\(a*(a+1)/2+k1*b*(b+1)/2+k2*b\),显然中间项是\(b/2\)的倍数,且k2对k1具有单调性,所以直接对中间项的系数二分答案之后找到离c最近的答案,check加减1即可,具体式子和细节看代码。

#include<bits/stdc++.h>
#define int long long
using namespace std;
int t,a,b,c,B;
bool chk(int x){return a*x+(x-1)*x/2*(b-1)>=(c-x*B)/b;}
int solve(){
	B=b*(b+1)/2;
	if(a*(a+1)/2>c)return -1;
	c-=a*(a+1)/2;
	if(!b)return 0-!!c;
	int L=-1,R=(c+B-1)/B;
	while(L+1<R){
		int M=(L+R)>>1;
		if(chk(M))R=M;
		else L=M;
	}
	if(R*B<=c&&(c-R*B)%b==0)return R;
	R++;
	if(R*B<=c&&(c-R*B)%b==0)return R;
	return -1;
}
signed main(){
	freopen("bike.in","r",stdin);
	freopen("bike.out","w",stdout);
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>t;
	while(t--)cin>>a>>b>>c,cout<<solve()<<'\n';
}

T2

首先答案单调不升,考虑双指针,问题在于怎么check,类似二分答案转01的trick,考虑将>=当前答案的设为1,删除标记设为-1,那么条件就是存在一个后缀>=0(非常聪明的转化,赛时一直想着维护前缀, 实际上这种类似于前缀max(0,x)状物就应该正难则反考虑后缀,这个trick较为常用),那么就是很简单了,单点改,然后维护全局后缀和max,可以对偶成前缀改,求全局max就做完了。

T3

这种同余/异或路径存在性问题,考虑凑成抵消的东西,然后简化题面。

经过若干次推理,可以转化成保留原图一个生成树,然后是所有只有一条非树边的所有环的gcd,然后用exgcd维护一下就行了。

10.9

T1

对于系主任来说,肯定选择 \(b_i\) 最多的这几个,然后在这基础上选\(a_i\)最小的,那我们就先按照系主任的要求排序,那我们无论选什么,他都会选下标最大的k个,那如果我们优先选所有a_i最大的,那我们需要保证一定会有剩下的p-k个数留给我们垫底,所以我们就可以先预留这些数,然后让其他数按照a_i排序,这样我们就能取到a_i最大了。那么剩下的数还要b_i最大怎么办呢?我们按照a_i排序的时候,在a_i相同时优先选择下标大的,这样左边就能预留多一点空间给我们选b_i最大的,由于左边剩下的一定不选,所以直接在这里面挑所有b最大的就行了。

T2

首先达到目标状态最多推两下。没推过的箱子是好判断的。首先如果推一下,肯定所有箱子都顶到某一条边上了,那么这条边的方向是没有自由度的,垂直于这条边的方向可以任意选,所以就是\(\binom{n}{c_i}\),然后推两下的肯定聚集在某个角落, 四个角是等价的,所以假设在右上角,那么有两种操作可以到左上角,一个是先左再上,一个是先上再左。考虑什么东西先左再上能到达这个状态,首先考虑有多少种中间状态能到达当前状态,那就相当于每一列的数量都是一样的,并且全部都顶到左边了,全部都顶到左边意味着什么?意味着每一列的位置都被左边一列完全包含,所以方案数是\(\binom{c_{i-1}}{c_i}\),然后再考虑有多少初始状态能到达中间状态?发现跟上一种一步到达的一模一样,注意到方案数跟中间状态的形态无关,只跟中间状态\(c_i\)有关,而所有中间状态的\(c_i\)只是最终状态重排了一下,所以说最终方案数等于两步相乘。然后考虑去重,有可能一个状态既能先左再上,又能先上再左,发现这个等价于最终状态散开,但是相对位置不变,发现这个就是两维的\(\binom{c_{i-1}}{c_i}\)相乘。

代码不需要显式维护图形,只需要\(c_i\)就能算答案了。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2005,p=1e9+7;
int n,m,C[N][N],a[N],b[N],f1,f2,g1,g2;
char s[N][N];
void M(int &x,int y){x=x*y%p;}
signed main(){
	freopen("box.in","r",stdin);
	freopen("box.out","w",stdout);
	ios::sync_with_stdio(0),cin.tie(0);
	n=2e3;
	for(int i=0;i<=n;i++)C[i][0]=1;
	for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)C[i][j]=(C[i-1][j-1]+C[i-1][j])%p;
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>(s[i]+1);
	int A=0,B=0;
	for(int o=-1;o<=1;o+=2){
		bool q=1;
		for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)if(s[i][j]=='#'&&s[i+o][j]=='.')q=0;
		A|=q;
	}
	for(int o=-1;o<=1;o+=2){
		bool q=1;
		for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)if(s[i][j]=='#'&&s[i][j+o]=='.')q=0;
		B|=q;
	}
	for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)a[i]+=s[i][j]=='#';
	for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)b[j]+=s[i][j]=='#';
	sort(a+1,a+n+1),a[n+1]=m,sort(b+1,b+m+1),b[m+1]=n,f1=f2=g1=g2=1;
	for(int i=1;i<=n;i++)M(f1,C[a[i+1]][a[i]]);
	for(int i=1;i<=m;i++)M(f2,C[b[i+1]][b[i]]);
	for(int i=1;i<=n;i++)M(g1,C[m][a[i]]);
	for(int i=1;i<=m;i++)M(g2,C[n][b[i]]);
	if(!A&&!B)cout<<1;
	else if(A&&B)cout<<(f1*g2%p+f2*g1%p-f1*f2%p+p)%p;
	else if(A)cout<<g2;
	else cout<<g1;
}

T3

仙人掌少见的好题。

首先值域这么小,肯定有问题,考虑按照值域把图拆成若干个子图,每个子图是原图边权\(>=x\)的生成子图,那么答案是每一个子图的连通块数量减去上一个子图的连通块数量(等价于当前权值的边的合并次数)乘上当前权值。问题就转化为了如何维护连通块数量?考虑维护变化量。首先只观察当前列和上一列形成的类二分图,看看这个二分图和上个二分图为什么会有差别?我们发现,差别就在于上个二分图给这个二分图提供了相较于上一个二分图没有的联通性,注意到我们可以直接在原图上面修改,所以说根本没必要维护整个图,只需要动态维护这个二分图就行。详细的描述一下变化量,就是类似于上一个图的某些两个右部点之间联通了,会导致这个图相较于上一个图多了一个左部点之间的连边,那我们只需要每一次维护是否有两个右部点被联通就行了,如果我们普通维护这个并查集,无法知道这个连边对应的两个集合中是否都含有右部点,我们有一个比较巧妙的方法就是将右部点的编号设为左部点+n,然后代表元取集合的最大值,这样就可以快速判断了。维护答案也是一个非常巧妙的点,通过维护当前包含右部点的连通块个数s1,这个二分图的连通块数量s2,最后算出原图的答案s3。

想清楚之后代码维护极其清新易懂,而且写起来没有细节,最短解。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+5,W=35;
int n,l,m;
struct graph{
	int s1,s2,s3,f[N];
	basic_string< pair<int,int> > g,h;
	void init(){s1=s3=n,s2=2*n;for(int i=1;i<=2*n;i++)f[i]=i;}
	int find(int x){return f[x]==x?x:f[x]=find(f[x]);}
	void calc(){
		s3-=s1;
		for(auto [u,v]:g){
			u=find(u),v=find(v);
			if(u!=v){
				s2--;
				if(u<v)swap(u,v);
				f[v]=u;
				if(u>n&&v>n)h+={u-n,v-n},s1--;
			}
		}
		g=h,h.clear(),s3+=s2;
	}
}G[W];
signed main(){
	freopen("tree.in","r",stdin);
	freopen("tree.out","w",stdout);
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>l>>m;
	for(int i=1;i<=30;i++)G[i].init();
	for(int i=1;i<=m;i++){
		int u,v,w;cin>>u>>v>>w;
		for(int j=w;j<=30;j++)G[j].g+={u,v+n};
	}
	for(int i=1;i<=l;i++){
		int x=0;
		for(int j=1,y=(i+1)*n;j<=30;j++)G[j].calc(),x+=j*(y-G[j].s3),y=G[j].s3;
		cout<<x<<'\n';
	}
}

T4

一场出两个好题,这场感觉是最高质量场了。

想一下怎么样的插入才是合法插入。显然,一个数插的位置后面必须小于等于这数才能让字典序变大,发现等于也是假的,因为等价于直接插到连续段的末尾

10.17

exp:100+100+100+75=375
real:96+100+24+45=265

T1

就相当于dfs序,那么这个显然是一个临项交换性质,差不多就是对于x和y子树,x在y前面的条件是\(max(val_x,val_y+2siz x<max(val_y,val_x+2siz_y))\) 注意到这个东西满足严格弱序,所以直接排序就行了,注意去掉父亲,因为(0,0)和任何数比较都是小于,所以就不满足严格弱序了。

T2

发现正着算不好算,考虑把贡献算到最小的\(f(y)=x\)的y上,我们发现y有贡献,当且仅当\(f(y)>f(y-1)\),等价于\(\sum _i[x_i|y]>0\),这是一个很经典的容斥式子,我们先二分出\(y_{max}\),然后答案就是\(\sum_{s}(-1)^{|s|-1}\frac{y_{max}}{lcm_{x \in s}}\),注意lcm要从lowbit的位置转移过来,复杂度可以少一个n,\(O(2^n \log V)\),log是gcd的复杂度,可以用记忆化搜索减少常数。

T3

回文串不好做,考虑反串,发现原操作就是原串和反串的某个区间对位合并,对两个串建出st表,对每个st表记录并查集节点,每一次操作就暴力合并到最底层,如果当前已经在同一个并查集中就不合并,由于一共有\(n\log n\) 个节点,所以最多就合并这么多次。那查询怎么做,发现如果直接查两个节点是否在同一个并查集中是错的,因为有可能儿子分别被合并,但是这两个节点有可能没被合并,所以我们需要在查询的时候pushup,然后就做完了。总复杂度是\(O(n\log n\alpha(n)+q)\),空间\(O(n\log n)\)

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,m,q,tot,st[20][N],f[N*20],sz[N*20];
int find(int x){return f[x]==x?x:f[x]=find(f[x]);}
void upd(int u,int v){
	if(sz[u]<sz[v])swap(u,v);
	f[v]=u,sz[u]+=sz[v];
}
void merge(int x,int a,int b){
	int u=find(st[x][a]),v=find(st[x][b]);
	if(u==v)return;
	upd(u,v);
	if(x)merge(x-1,a,b),merge(x-1,a+(1<<(x-1)),b+(1<<(x-1)));
}
bool ask(int x,int a,int b){
	int u=find(st[x][a]),v=find(st[x][b]);
	if(u==v)return 1;
	if(!x)return 0;
	if(ask(x-1,a,b)&&ask(x-1,a+(1<<(x-1)),b+(1<<(x-1)))){upd(u,v);return 1;}
	return 0;
}
int main(){
	freopen("palindrome.in","r",stdin);
	freopen("palindrome.out","w",stdout);
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>q,m=n*2;
	for(int i=0;i<=16;i++)for(int j=1;j<=m-(1<<i)+1;j++)st[i][j]=++tot;
	for(int i=1;i<=tot;i++)f[i]=i,sz[i]=1;
	for(int i=1;i<=n;i++)upd(i,m-i+1);
	while(q--){
		int o;cin>>o;
		if(o==1){
			int l,r;cin>>l>>r;
			merge(__lg(r-l+1),l,m-r+1);
		}
		if(o==2){
			int l1,r1,l2,r2,x;cin>>l1>>r1>>l2>>r2;
			if(r1-l1!=r2-l2)cout<<"Not equal";
			else x=__lg(r1-l1+1),cout<<(ask(x,l1,l2)&&ask(x,r1-(1<<x)+1,r2-(1<<x)+1)?"Equal":"Unknown");
			cout<<'\n';
		}
	}
}

T4

首先中位数套路二分答案转01,然后注意到如果当前合法,那么操作次数最多为\(\log n\)次,因为每次你可以将1的区间长度至少*2-1,然后肯定如果两个操作被包含,那么操作大的区间一定更优,然后最后的操作区间一定会两两包含,因为如果相交,那一定包含更优,如果不相交,那么考虑操作序列最后两个不相交的区间,显然可以先不操作其中一个,如果操作区间要包含另一个了,再操作另一个,这样显然更优。

那么因为每一步过后都一定是只有一段全变成1,我们就可以dp了,设\(f_{i,l,r}\)表示第\(i\)步操作[l,r]是否合法,转移就是暴力枚举上一步的操作,显然复杂度太高了。

用一个经典转换dp状态和值的trick,设\(f_{i,l}\)表示第i步操作的左端点为l的最大右端点是多少,根据之前的性质,如果\(a<b,f_{i-1,a}>f_{i-1,b}\),那么b就是没用的,所以所有有用的区间都是不包含的,也就是左右端点单调递增,设\(c_{l,r}\)为操作[l,r]相较与原序列增加的权值,那么对于一个操作区间[l,r]的判定,就能变成\(s_{l-1}-\max_{[a,b] \in [l,r]}{c_{a,b}}<s_r\)。

考虑从右往左扫描线,加入上一次操作的区间,查询当前左端点对应的答案,如果有区间的权值小于当前加入的区间的权值,那么之前加入的区间一定不优,这启发我们维护维护从队头到队尾权值单调递减,区间从右往左的单调队列(刚刚的性质可以用来维护队尾),然后查询的时候就是查找从队头开始第一个合法的区间,问题如何维护队头,也就是快速判断一个区间是否合法以及找到这个区间,详细的刻画这个条件就是当前扫描线到i,对于一个队列里的区间[l,r],存在\(x\ge r,s_{i-1}-c_{l,r}<s_x\)。

我们发现这个东西等式左边跟x无关,所以说就可以预处理一个\(g(y)\)表示\(s_x\ge y\)的最大的x。判定条件就转化为了\(g(s_{i-1}-c_{l,r}+1)\ge r\),首先g肯定是单调递减的,-c是关于r不变,问题就在于s,我们发现如果\(x<y,s_{x-1}<s_{y-1}\),那么y作为左端点肯定不优,所以s也是单调递增的,那么左边式子就是单调递减的,可以用来维护队头(这里说的单调性都是从右往左)

时间复杂度\(O(n \log^2 n)\),两个log分别是次数和二分答案。

posted @ 2026-10-19 09:07  Lqs314  阅读(9)  评论(0)    收藏  举报