初三赛季杂题泛做

发布时间 2023-07-12 17:22:17作者: Anticipator

模拟赛1006 T3

可以发现交集点选的边一定是它的最小生成树上的,2^n爆算即可

模拟赛1006 T4

这种题做过好多遍了,一个广为人知的结论就是k选的区间一定是k+1选的区间的前缀,线段树上二分即可

模拟赛1007 T3

考虑每个不同的字符串前缀都会作为一个trie树上的节点,于是表示出每个前缀t至少出现一次的概率,加起来就是期望了。柿子用背包算。

设T中字符分别出现(T0,..Tn)次

P(T是S前缀)=

$\frac {A_{\sum Si}^{\sum ti}}{\prod A_{Si}^{Ti}}$

模拟赛1007 T4

自己看题解

计蒜客1007 T4

i=k时候,答案不超过n/k

线段树上二分即可

CF 1572D

建出二分图。

考虑每加入一条边,删去相邻边2n-1条,所以k-1次之后一定可以选第(2n-1)(k-1)+1大的边,这个数可以直接跑BM

模拟赛1011 T1

dp方程列出来,然后发现肯定是越往远处扩展越好。

考虑到[i,j]与[i+1,j]不同的地方只有多了[i]...j]

于是变长的那部分只能是与i往右的部分匹配了,否则[i+1,j]也会拓展到它。

双指针即可

模拟赛1011 T2

直接容斥会挂,考虑到把柿子写出来,相当于把Ci->Cj边权为-F[Ci][Cj],于是就是求从超级源到超级汇的路径乘积和了。

容斥不一定要2^n的啊啊啊

模拟赛1013 T1

把(a,b,c)钦定选a,于是变成选择y个(b-a)和z个(c-a)使得和最大。

微扰一下

设(b-a)=e,(c-a)=f

Ei+Fj>Ej>Fi

Ei-Fi>Ej-Fj

按Ei-Fi排序,记录前后缀和,枚举分界点取最大答案即可。

Ohhhhh傻逼题

Complete the MST

显然发现除了一条边填x之外其他都填0

肯定是想办法把0全部加入,x不加

当图比较稠密的时候,暴力枚举x填在哪条边,跑kruscal

O(nsqrt(n))

当图比较稀疏的时候,补图必定有环,环边上的0没有用,随便取一条换成x,最后再跑一遍kruscal。(相当于把0放下来到别的地方了。)

CF1031D Minium Path

模拟赛里做到了这种题。

总结一下套路:分层、BFS,贪心

求网格图中到某点路径字典序最小:

对于每层点,把右边和下面的点加入,这些是下一层可能用到的。

这些点里选最小的那些,所以每层点之间都是相等的

把这些点入队,一直做是N^2的

模拟赛1016 T1

给图分层,枚举前导0(即只通过0边BFS到的点),把最低的前导0加入,每层到下一层的贪心取最小的边(u,v),把v入队。

模拟赛1018 T2

记inv[i]为i前面比i大的个数,不难发现一个inv数组与一个排列一一对应,于是直接用某题做就行了。

模拟赛1020 T2

对每个k,求$ \max(A_i+B_j) (i|j=k)$

k<=2^18

yjj的算法很NB,就是一起一位一位填(i,j,k),保证i|j==k并且i,j无交集。填满n位之后算a[i]+B[j] (此处B[j]表示B的超集中的k的子集的b的最大值)

在dfs的时传入三个指针(a,b,ans)每次+mid就是移动指针,结束时填到相应位置。当(a+mid,b,ans+mid)时可以加入包含b的数,用它们+mid的位置的b更新超集最大值。


回望过去,当年那场联合省选给我的启示意义太大了

无论如何要看清题面,模拟样例

无论如何要敢于尝试、相信自己的水平

无论如何要善于转化题面,相信一切都是可做的!

如果发现不是很可做,那一定有结论!

最重要的:无论如何,不能畏难!

遥想当年我Day1再次高估难度,降智得连T1都不会,一出考场发现大家都200+,很震惊,很难过。

于是Day2考场上就以为都是傻逼题(虽然这题真是傻逼题),没看清楚题面就开始写,白写了两个小时错题。

这题我想出了2^nnn*m 的做法,还过了大样例,但官方数据挂成了60分(md我2.5h写的和别人0.5h写的分数一样),并且至今不知道错在哪(也许是没开ll?还是边界问题?还是常数太大?)考场代码早就不知道放哪去了,反正至今都是一个大遗憾。

于是,今天,我决定复盘那场完全失败的联合省选!

鉴于D1T1、D1T3、D2T1之前已经复盘过了,而这道满载着遗憾的D2T2还没过,于是决定复盘。

显然打N!暴力的时候发现只要贪心$B_i=B_{i-1}+(A_i-A_{i-1})+(i-1<i)$ 最后$\sum Bi \le M$ 就OK了

于是差分一下

$\sum [(A_i-A_{i-1})+(i-1<i)]*(n-i+1) <= M$

于是这题就做完了,$O(2^N \times N \times N \times M)$

tmd要开ll

#include<bits/stdc++.h>
using namespace std;
int n,m;
const int N=15,M=505;
int a[N]; 
// \sum max(a_i-1-a_i,0) * (n-i+1)
typedef long long ll;
ll f[1<<N][N][M];
int main(){
	int i,j,k,p,mx=0,mxf;
	cin>>n>>m;
	for(i=1;i<=n;i++){
		scanf("%d",&a[i]);
		if(a[i]>mx){
			mx=a[i];
			mxf=i;
		}
	}
	for(i=1;i<=n;i++)
		if(n*(mx-a[i]+(mxf<i))<=m)f[1<<i-1][i][n*(mx-a[i]+(mxf<i))]=1;
	for(i=0;i<(1<<n);i++){
		int cnt=0,o=i;
		while(o)cnt++,o-=o&-o;
		if(cnt<=1)continue;
		for(j=1;j<=n;j++)
			if(i>>j-1&1)
				for(k=1;k<=n;k++)
					if((i^(1<<(j-1)))>>k-1&1){
						int del_cur=(n-cnt+1)*(max(a[k]-a[j]+(k<j),0));
						for(p=del_cur;p<=m;p++)
							f[i][j][p]+=f[i^(1<<(j-1))][k][p-del_cur];
					}
	}
	ll ans=0;
	for(i=1;i<=n;i++)
		for(j=0;j<=m;j++)
			ans+=f[(1<<n)-1][i][j];
	cout<<ans<<endl;
	return 0;
}

马上AFO了集齐九九八十一篇题解说不定能渡劫。。。

看到这题然后不会做,怎么办

合并操作啊

一个巧妙的建模是把操作转换成等腰直角三角形的覆盖。

一开始所有点都是底边为[i-a[i],i+a[i]]的等腰直角三角形。

很显然,当两区间有交的时候合并是优的,没交的时候合并是不优的

于是就做完了

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2000005;
int n;
int L,R,top,sl[N],sr[N];
int main(){
	int i;
	cin>>n;
	for(i=1;i<=n;i++){
		int x,l,r;
		scanf("%d",&x);
		if(!x)continue;
		l=i-x,r=i+x;
		if(l>R){
			sl[++top]=L;
			sr[top]=R;
			L=l,R=r;
		}else{
			L=min(L,l),R=max(R,r);
			while(top&&L<=sr[top])
				L=min(L,sl[top--]);
		}
	}
	sl[++top]=L,sr[top]=R;
	int ans=0;
	for(i=1;i<=top;i++)ans+=(sr[i]-sl[i]+1)>>1;
	cout<<ans<<endl;
	return 0;
}

这可能是OI生涯最后的文章了

upd:渡劫失败,但复活了


MOMENT OF BLOSSOM

无向图,数据还那么大,还是异或,答案很显然是在随便一颗生成树上的。

感性理解一下,如果所有操作都集中在一条路径上,那抵销的肯定更多,所以结论是对的。

如果可以,直接暴力跳LCA即可。

否则,异或出来为1的那些路径所组成的连通块(是一片森林),每次选择一条路径删去,最少删几次?

这是个经典问题,ans=奇数点个数/2

code:

#include<bits/stdc++.h>
using namespace std;
int n,m;
const int N=6e5+5;
int cnt,h[N],nxt[N],to[N],f[N],fa[N],a[N],X[N],Y[N],b[N],dep[N];
void add(int x,int y){
	nxt[++cnt]=h[x];
	h[x]=cnt;
	to[cnt]=y;
}
int gf(int x){
	return x==f[x]?x:f[x]=gf(f[x]);
}
void dfs(int u,int pa){
	int i;
	fa[u]=pa;dep[u]=dep[pa]+1;
	for(i=h[u];i;i=nxt[i]){
		int v=to[i];
		if(v!=pa)dfs(v,u);
	} 
}
int du[N],ans,dao[N],opt;
int cmp(int x,int y){
	return dep[x]<dep[y];
}
int main(){
	cin>>n>>m;
	int i;
	for(i=1;i<=n;i++)f[i]=i;
	for(i=1;i<=m;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		int fx=gf(x),fy=gf(y);
		if(fx^fy){
			f[fx]=fy;
			add(x,y),add(y,x);
		}
	}
	dfs(1,0);
	int q;cin>>q;
	for(int j=1;j<=q;j++){
		scanf("%d%d",&X[j],&Y[j]);
		for(i=X[j];i;i=fa[i])a[i]^=1;
		for(i=Y[j];i;i=fa[i])a[i]^=1;
	} 
	int tt=0;
	for(i=2;i<=n;i++)tt+=a[i];
	if(tt){
		puts("NO");
		for(i=1;i<=n;i++)du[i]^=a[i],du[fa[i]]^=a[i];
		for(i=1;i<=n;i++)ans+=du[i]&1; 
		cout<<ans/2<<endl;
	}else{
		puts("YES");
		for(int j=1;j<=q;j++){
			int x=X[j],y=Y[j];
			if(dep[x]>dep[y])swap(x,y);
			int p=1,opt=0;
			for(i=1;i<=n;i++)b[i]=0;
			for(i=x;i;i=fa[i])b[i]^=1;
			int LCA=0;
			for(i=y;i;i=fa[i]){
				b[i]^=1;
				if(b[i]==0&&!LCA)LCA=i;
			}
			for(i=x;i!=LCA;i=fa[i])p++;
			for(i=y;i!=LCA;i=fa[i])p++;
			printf("%d\n",p);
			for(i=X[j];i!=LCA;i=fa[i])
				printf("%d ",i);
			printf("%d ",LCA);
			for(i=Y[j];i!=LCA;i=fa[i])dao[++opt]=i;
			for(i=opt;i>=1;i--)
				printf("%d ",dao[i]);
			puts("");
		}
	}
	return 0;
}

SPANNING TREE QUERIES

肉眼可见,答案在很少几段区间内都是等差数列,为什么呢?

因为对于任意的两条边$(i,j)$,当且仅当$2x < A_i+A_j+1 $时,kruscal的时候会先尝试选$i$,否则先选$j$

这样答案就与小于x的边数线性相关,所以是等差数列。

所以称任意$(i,j)$的中点为关键点,只有$m2$个点,可以$m3$暴力算出这几个点的答案。而所有询问的答案一定是以这些答案为首项的等差数列的第几项。

所以对于每个q,它的答案容易通过lowerbound对应的关键点的答案算出来。

注意求的是所有答案的异或和,于是排个序再双指针算就可以做到线性了

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=200005,M=1e7+5;
ll n,m;
struct node{
	ll x,y,z,zz;
}a[N],b[N];
ll cmp(node u,node v){
	if(u.z==v.z)return u.zz>v.zz;
	return u.z<v.z;
}
ll h,c[N],f[N];
ll gf(ll x){
	if(f[x]==x)return x;
	return f[x]=gf(f[x]);
}
ll s[N],ans,q[M],t[N],F[M];
int main(){
	cin>>n>>m;
	int i,j;
	c[++h]=0;
	for(i=1;i<=m;i++){
		scanf("%lld%lld%lld",&a[i].x,&a[i].y,&a[i].z),a[i].zz=a[i].z;
	c[++h]=a[i].z;
	}
	c[++h]=2e9;
	for(i=1;i<=m;i++)
	for(j=1;j<i;j++)
	c[++h]=(a[i].z+a[j].z+1)/2;
	sort(c+1,c+h+1);
	h=unique(c+1,c+h+1)-c-1;
	sort(a+1,a+m+1,cmp);
	for(i=1;i<=h;i++){
		for(j=1;j<=m;j++){
			b[j]=a[j];
			b[j].z=abs(c[i]-b[j].z);
		}
		sort(b+1,b+m+1,cmp);
		for(j=1;j<=n;j++)f[j]=j;
		int	tot=0;
		for(int j=1;j<=m;j++){
			int fx=gf(b[j].x),fy=gf(b[j].y);
			if(fx!=fy){
				f[fx]=fy;
				if(tot<n-1){
					tot++;
					if(b[j].zz<=c[i])s[i]-=b[j].zz,t[i]++;
					if(b[j].zz>c[i])s[i]+=b[j].zz,t[i]--;
				}
			}
		}
	}
	ll pos,k,A,B,C;
	cin>>pos>>k>>A>>B>>C;
	for(i=1;i<=pos;i++)scanf("%lld",&q[i]); 
	for(i=pos+1;i<=k;i++)q[i]=(q[i-1]*A+B)%C;
	sort(q+1,q+k+1);
	ll ans=0;int p=1;
	for(i=1;i<=k;i++){
		while(p<h&&c[p+1]<=q[i])p++;
		ans^=(s[p]+t[p]*q[i]);
	}

	cout<<ans<<endl;
	return 0;
}

CF1632E

mmd怎么现场比赛的2500+每次都不会做

加边肯定是加(1,x),如果二分一个答案A,那dep>A的一定要被影响到

那这些dep[u]=X+dis(u,x),而dep[u]<=A,所以dis(u,x)<=A-X

总之,要在树上一堆点中找到一个x使得max(dis(u,x))最小。

这是个经典结论,x取直径中点就行了,所以要找到最小的A使得$A-\frac{D+1}{2} >=X$

显然,由于可二分性,不等式左随着A的增大而增大,右随X增大而增大

于是双指针搞定。至于维护直径这事,枚举LCA,显然一定是选最深和次深的链(最早且最长)。

code(Starline):

#include <bits/stdc++.h>
using namespace std;
#define pb push_back
const int N = 3e5 + 5;
vector <int> adj[N];
int T, n, ans, d[N], mx[N], mx2[N], f[N];
void dfs(int u, int fa) {
	mx[u] = mx2[u] = 0;
	for (auto v : adj[u]) if (v != fa) {
		d[v] = d[u] + 1, dfs(v, u);
		if (mx[v] + 1 > mx[u]) mx2[u] = mx[u], mx[u] = mx[v] + 1;
		else if (mx[v] + 1 > mx2[u]) mx2[u] = mx[v] + 1;
	}
	f[d[u] + mx2[u]] = max(f[d[u] + mx2[u]], mx2[u] + mx[u]);
}
 
int main() {
	scanf("%d", &T);
	while (T--) {
		scanf("%d", &n);
		for (int i = 1; i <= n; i++) adj[i].clear();
		fill(f, f + 1 + n, 0);
		for (int i = 1, x, y; i < n; i++)
			scanf("%d%d", &x, &y), adj[x].pb(y), adj[y].pb(x);
		dfs(1, 0);
		for (int i = mx[1] - 1; i >= 0; i--) f[i] = max(f[i + 1], f[i]);
		for (int i = 1, j = 0; i <= n; i++) {
			while (j < mx[1] && (f[j + 1] + 1) / 2 + i > j) j++;
			printf("%d ", j);
		}
		printf("\n");
	}
	return 0;
}

CF1634E&F 题解

E没什么好解的,注意欧拉回路只能搜一个连通块(因为这个我调了n年)看来以后还是要冷静冷静再冷静。

code:

#include<bits/stdc++.h>
using namespace std;
const int M=200005;
int T;
vector<int> g[M<<2];
int tot,cnt[M<<2],a[M<<2],tmp[M<<3],ans[M<<4],top,vis[M<<3];
map<pair<int,int>,int> mp;
vector<int> edge[M<<3],res[M<<3];
vector<int> lac[2][M<<3]; 
void Euler(int u){
	while(1){
		while(tmp[u]<edge[u].size()&&!mp[make_pair(u,edge[u][tmp[u]])])tmp[u]++;
		if(tmp[u]>=edge[u].size())break;
		int v=edge[u][tmp[u]];
		mp[make_pair(u,v)]--;
		mp[make_pair(v,u)]--;
		++tmp[u];
		Euler(v);
	}
	ans[++top]=u;
	vis[u]=1;
}
int main(){
	cin>>T;
	int i,j;
	for(i=1;i<=T;i++){
		int n;
		scanf("%d",&n);
		for(j=1;j<=n;j++){
			int x;
			scanf("%d",&x);
			g[i].push_back(x);
			a[++tot]=x;
		}
	}
	sort(a+1,a+tot+1);
	tot=unique(a+1,a+tot+1)-a-1;
	for(i=1;i<=T;i++){
		for(j=0;j<g[i].size();j++){
			int w=g[i][j];
			g[i][j]=lower_bound(a+1,a+tot+1,w)-a;
			cnt[g[i][j]]++;	
			edge[i].push_back(T+g[i][j]);
			edge[T+g[i][j]].push_back(i);
			mp[make_pair(i,T+g[i][j])]++,mp[make_pair(T+g[i][j],i)]++;
			lac[0][g[i][j]].push_back(i);
			lac[1][g[i][j]].push_back(j); 
		}
	}
	for(i=1;i<=tot;i++){
		if(cnt[i]&1){
			puts("NO");
			return 0;
		}
	}
	puts("YES");
	for(i=1;i<=T+tot;i++)
		sort(edge[i].begin(),edge[i].end());
	for(i=1;i<=T+tot;i++)
		if(!vis[i])Euler(i);
	for(i=top;i>=2;i--)
		if(ans[i]<=T&&ans[i-1]>T)
			res[ans[i-1]-T].push_back(ans[i]);
	for(i=1;i<=tot;i++){
		sort(res[i].begin(),res[i].end());
		int k=0;
		for(j=0;j<lac[0][i].size();j++){
			if(lac[0][i][j]==res[i][k]){
				g[lac[0][i][j]][lac[1][i][j]]=-1; 
				if(k<res[i].size()-1)k++;
				else{break;}
			}
		}
	}
	for(i=1;i<=T;i++){
		for(j=0;j<g[i].size();j++)
			putchar(g[i][j]==-1?'L':'R');
		putchar('\n');
	}
	return 0;
} 

F

惊了,原来根本不用ds

设$C_i=A_i-B_i$

$D_i=C_i-C_{i-1}-C_{i-2}$

当且仅当D_i全为0时,答案为YES

考虑加一段对D_i的影响

Dl will increase by 1,

Dr+1 will decrease by Fr−l+2, and

Dr+2 will decrease by Fr−l+1.

Fibonacci addition on B can be handled in a similar way.

看来ds题还是要先想差分


P8118

其实不用左偏树,看一下三倍经验(P4331、P2893)的第一篇题解,发现DP过程可以通过大根堆来优化。这样不仅更好写,而且更加方便后续的min。

读入的时候把Ai -= k*i,就是原题了

然后每次要取个后缀最小值,这个直接做显然T飞(当然我不知道有没有什么高明做法)。于是反过来对每个ai,求取到哪个前缀的时候可以使ai开始小于bi,差分一下就行了。

线段树上二分,直接单log解决


[JRKSJ R4] risrqnis 题解

人在西安,刚下高铁qwq。

暴力算法(sub0):

离散化,用并查集维护当前值域连续段,新增的一段值域暴力加入这个值对应的下标,树状数组上+1,询问内直接区间求和。

O(nmlogq)

根号分治:

  • 对于连续段数<= sqrt q 的, 离线二维数点,使用O(sqrt)修改,O(1)查询的分块
  • 对于连续段数> sqrt q 的,用暴力算法,将树状数组换成O(1)修改,O(sqrt)查询的分块。

O((n+q)(sqrt n+ sqrt q))

人在西安,刚下高铁qwq。

暴力算法(sub0):

离散化,用并查集维护当前值域连续段,新增的一段值域暴力加入这个值对应的下标,树状数组上+1,询问内直接区间求和。

O(nmlogq)

根号分治:

  • 对于连续段数<= sqrt q 的, 离线二维数点,使用O(sqrt)修改,O(1)查询的分块
  • 对于连续段数> sqrt q 的,用暴力算法,将树状数组换成O(1)修改,O(sqrt)查询的分块。

O((n+q)(sqrt n+ sqrt q))

其实不用左偏树,看一下三倍经验(P4331、P2893)的第一篇题解,发现DP过程可以通过大根堆来优化。这样不仅更好写,而且更加方便后续的min。

读入的时候把Ai -= k*i,就是原题了

然后每次要取个后缀最小值,这个直接做显然T飞(当然我不知道有没有什么高明做法)。于是反过来对每个ai,求取到哪个前缀的时候可以使ai开始小于bi,差分一下就行了。

线段树上二分,直接单log解决