济南CSP-J刷题营集训内容

Day1(基础算法)

T1

求和可以用前缀和。
求平均值时,特判是否整除而输出结果。
求方差,我们直接用他给的公式以分数形式算出结果,维护两个分子和分母,通分相减后特判输出。
注意要输出最简分数,所以我们用 \(\text gcd\) 约分。

#include<bits/stdc++.h>
#define gt getchar
#define pt putchar
#define int long long
using namespace std;
int a[100005];
int sum1[100010],sum2[100010];
int fc1,fc2;
int gcd(int a,int b){
	return(b==0?a:gcd(b,a%b));
}
int lcm(int a,int b){
	return a*b/gcd(a,b);
}
signed main(){
	int n;
	scanf("%lld",&n);
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		sum1[i]=a[i]+sum1[i-1];
		cout<<sum1[i]<<' ';
		sum2[i]=(a[i]*a[i])+sum2[i-1];
		if(sum1[i]%i==0){
			cout<<sum1[i]/i<<' ';
		}
		else{
			int g=gcd(sum1[i],i);
			cout<<sum1[i]/g<<'/'<<i/g<<' ';
		}
		
		int fm1=i;
		int fz1=sum1[i];
		int g=gcd(fm1,fz1);
		fm1/=g;
		fz1/=g;
		fz1=fz1*fz1;
		fm1=fm1*fm1;
		int fm2=i;
		int fz2=sum2[i];
		int l=lcm(fm2,fm1);
		int x=l/fm2,y=l/fm1;
		fm2*=x;
		fz2*=x;
		fm1*=y;
		fz1*=y;
		int fzans=abs(fz2-fz1);
		if(fzans%fm1==0)cout<<fzans/fm1<<'\n';
		else{
			int k=gcd(fzans,fm1);
			cout<<fzans/k<<'/'<<fm1/k<<'\n';
		}
	}	
	return 0;
}

T2

暴力 \(35分\)
直接枚举每一个二元组,然后算出 \(a_j + b_j \times c_i\),然后排序,输出第 \(k\) 个数即可、

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e5+10;
int a[MAXN],b[MAXN],c[MAXN];
vector<int>x;
signed main(){
	int n,k;
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=n;i++)cin>>b[i];
	for(int i=1;i<=n;i++)cin>>c[i];
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			x.push_back(a[j]+b[j]*c[i]);
		}
	}
	sort(x.begin(),x.end());
	cin>>k;
	cout<<x[k-1];
	return 0;
}

正解:二分
我们把序列 \(c\) 排序,二分答案查找在序列 \(c\) 中最后一个不大于 \(mid\) 的位置,寻找位置等于 \(k\) 的位置,更新答案,输出。

#include<bits/stdc++.h>
#define int long long
using namespace std; 
int a[100010],b[100010],c[100010];
int n;
int check(int x){
	int sum=0;
	for(int i=1;i<=n;i++)
		if(x-a[i]>=0)sum+=upper_bound(c+1,c+n+1,(x-a[i])/b[i])-c-1;
	return sum;
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=n;i++)cin>>b[i];
	for(int i=1;i<=n;i++)cin>>c[i];
	sort(c+1,c+n+1);
	int l=0,r=1e18+1e9;
	int k;
	cin>>k;
	while(l<r){
		int m=(l+r)>>1;
		if(check(m)>=k)r=m;
		else l=m+1;
	}
	cout<<r;
	return 0;
}

T3

暴力 \(30分\)
枚举每一个 \(a,b,c\),从小到大枚举可保证有序,判断 \(q\) 是否大于 \(1e5\),然后按规定输出。

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5;
struct pair3{
	int f,s,t;
};
vector<pair3>x;
signed main(){
	int n,n3,p;
	cin>>n>>n3>>p;
	const int n0=n*n+1;
	for(int i=1;i<=p;i++){
		for(int j=0;j<=p;j++){
			for(int k=1;k<=p;k++){
				int n1,n2;	
				n1=n0%i; 
				n2=n1+j;
				if(n2%k==n3){
					pair3 y={i,j,k};
					x.push_back(y);
				}
			}
		}
	}
	int q=x.size();
	if(q>100000){
		cout<<q<<'\n';
		for(int i=0;i<100000;i++){
			cout<<x[i].f<<' '<<x[i].s<<' '<<x[i].t<<'\n';
		}
	}
	else{
		cout<<q<<'\n';
		for(int i=0;i<q;i++){
			cout<<x[i].f<<' '<<x[i].s<<' '<<x[i].t<<'\n';
		}
	}
	return 0;
}

正解:
还能用二分你敢信

T4

不会QAQ

最后只得了 \(150\) pts,太蒻了。

Day2(搜索)

T1

直接枚举全排列,每次枚举所有数,如果相邻每个数的差都不等于 \(k\),把答案加一。

#include<bits/stdc++.h>
#define int long long
using namespace std;
int a[20];
signed main(){
	int n,k;
	cin>>n>>k;
	for(int i=1;i<=n;i++)a[i]=i;
	int cnt=0;
	do{
	    bool f=false;
		for(int i=2;i<=n;i++)
			if(abs(a[i]-a[i-1])==k)f=true;
		if(!f)cnt++;
	}while(next_permutation(a+1,a+n+1));
	cout<<cnt;
	return 0;
}

T2

\(80分\)
\(p\) 枚举全排列,算出每个数,然后去重,可以用 set。

#include<bits/stdc++.h>
#define int long long
using namespace std;
struct xiangliang{
	int x,y;
	xiangliang(){
		x=0;
		y=0;
	}
}a[105];
int p[105];
set<int>ans;
signed main(){
	int n;
	cin>>n;
	for(int i=1;i<=2*n;i++)
		cin>>a[i].x>>a[i].y;
	for(int i=1;i<=2*n;i++)p[i]=i;
	int cnt=0;
	do{
		int x=1;
		xiangliang y;
		y.x=1;
		y.y=1;
		for(int i=1;i<=2*n;i++){
			if(i&1){
				y.x=a[p[i]].x*x;
				y.y=a[p[i]].y*x;
			}
			else
				x=y.x*a[p[i]].x+y.y*a[p[i]].y;
		}
		ans.insert(x);
	}while(next_permutation(p+1,p+2*n+1));
	cnt=ans.size();
	cout<<cnt;
	return 0;
}

正解

将枚举全排列改成爆搜,剩下的和 \(80\) 分解差不多,都得用 set。

#include<bits/stdc++.h>
#define int long long
using namespace std;
bool vis[1005];
int a[1005],b[1005];
int n;
set<int>ans;
void dfs(int x,int y){
	while(x<=2*n&&vis[x])x++;
	if(x==2*n+1){
		ans.insert(y);
		return;
	}
	vis[x]=true;
	for(int i=x+1;i<=2*n;i++){
		if(!vis[i]){
			vis[i]=true;
			dfs(x,y*(a[x]*a[i]+b[x]*b[i]));
			vis[i]=false;
		}
	}
	vis[x]=false;
}
signed main(){
	cin>>n;
	for(int i=1;i<=2*n;i++)cin>>a[i]>>b[i];
	dfs(1,1);
	cout<<ans.size();
	return 0;

T3

这道题属于博弈论问题,我们用 \(min,max\) 搜索法,搜索状态即为两⼈的数字,直接暴⼒搜索,可以发现状态不多。
赛时估分 \(30\),实际正解通过。

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,a,b;
int search(int a,int b,char p){
	if(a>n||b>n){
		return a-b;
	}
	if(p=='A'){
		int x=0;
		int y=search(a+b,b,'B');
		if(a!=0&&b!=0)x=search(a*b,b,'B');
		return max(x,y);
	}
	if(p=='B'){
		int x=0;
		int y=search(a,a+b,'A');
		if(b!=0&&a!=0)x=search(a,a*b,'A');
		return min(x,y);
	}
	
}
signed main(){
	cin>>n>>a>>b;
	cout<<search(a,b,'A');
	return 0;
}

T4

赛时骗分失败,爆〇了。

正解:打表

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int a[10][10]={{1},
                        {1, 1},
                        {1, 2, 1},
                        {1, 6, 6, 1},
                        {1, 24, 90, 24, 1},
                        {1, 120, 2040, 2040, 120, 1},
                        {1, 720, 67950, 297200, 67950, 720, 1},
                        {1, 5040, 3110940, 68938800, 68938800, 3110940, 5040, 1},
                        {1, 40320, 187530840, 24046189440ll, 116963796250ll, 24046189440ll, 187530840, 40320, 1},
                        {1, 362880, 14398171200ll, 12025780892160ll, 315031400802720ll, 315031400802720ll,
                         12025780892160ll, 14398171200ll, 362880, 1}};
int n,m,k;
signed main(){
	cin>>n>>m>>k;
	if(k==0)cout<<1;
	else if(n!=m||k>n)cout<<0;
	else cout<<a[n][k];
	return 0;
}

得分:\(280pts\)

Day3(动态规划)

T1

十分简单的一般 DP。

#include<bits/stdc++.h>
using namespace std;
#define int long long
int a[5][100005];
int f[10][100005][10];
signed main(){
	int n;
	cin>>n;
	for(int i=1;i<=2;i++)
		for(int j=1;j<=n;j++)
			cin>>a[i][j];
	f[1][1][2]=a[1][1];
	f[2][1][2]=a[2][1];
	for(int j=2;j<=n;j++){
		for(int i=1;i<=2;i++){
			if(i==1){
				f[i][j][1]=max(f[i][j-1][2],f[i+1][j-1][2]);
				f[i][j][2]=max(f[i][j-1][1],f[i+1][j-1][2])+a[i][j];
			}
			else{
				f[i][j][1]=max(f[i][j-1][2],f[i-1][j-1][2]);
				f[i][j][2]=max(f[i][j-1][1],f[i-1][j-1][2])+a[i][j];
			}
		}
	}
	int ans=max(max(f[2][n][1],f[2][n][2]),max(f[1][n][1],f[1][n][2]));
	cout<<ans;
	return 0;
}

T2

最好想的就是用自己的值更新他人的值,注意边算边 \(mod\)

#include<bits/stdc++.h>
#define int long long
#define MOD 998244353
using namespace std;
int f[1005][1005];
int ans=0;
signed main(){
	int n;
	cin>>n;
	f[0][0]=1;
	for(int i=0;i<=n;i++){
		for(int j=0;j<=i;j++){
			f[i+1][j]=(f[i][j]%MOD+f[i+1][j]%MOD)%MOD;
			f[i][j+1]=(f[i][j]%MOD+f[i][j+1]%MOD)%MOD;
			f[i+1][j+1]=(f[i][j]%MOD+f[i+1][j+1]%MOD)%MOD;
		}
	}
	cout<<f[n][n]%MOD;
	return 0;
}

T3

gzy本意想让我们写 DP,但这题用记忆化搜索很轻松就过了,原题:滑雪。

#include<bits/stdc++.h>
#define int long long
using namespace std;
int dx[]={0,0,1,-1},dy[]={1,-1,0,0};
int n,a[1005][1005],f[1005][1005];
int dfs(int x,int y){
	if(f[x][y])return f[x][y];
	f[x][y]=1;
	for(int i=0;i<4;i++){
		int xx=dx[i]+x;
		int yy=dy[i]+y;
		if(xx<1||yy<1||xx>n||yy>n)continue;
		if(a[x][y]>a[xx][yy]){
			dfs(xx,yy);
			f[x][y]=max(f[x][y],f[xx][yy]+1);
		}
	}
	return f[x][y];
}
signed main(){
	cin>>n;
	int ans=-1;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			cin>>a[i][j];
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			ans=max(ans,dfs(i,j));
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++)
			cout<<f[i][j]<<' ';
		putchar('\n');
	}	
	return 0;
}

T4

爆搜 \(20分\)

#include<bits/stdc++.h>
using namespace std;
int a[100005];
int maxn=-1;
int n;
void dfs(int now,int x,int y){
	if(x==y)maxn=max(maxn,x);
	if(now>n)return;
	dfs(now+1,x+a[now],y);
	dfs(now+1,x,y+a[now]);
	dfs(now+1,x,y);
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];	
	dfs(1,0,0);
	cout<<maxn;
	return 0;
}

正解:
背包 \(+\) 滚动数组

#include<bits/stdc++.h>
#define int long long
using namespace std;
int a[1005],f[5][500005];
signed main(){
	int n,m=0;
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i],m+=a[i];
	int last=0,cur=1;
	for(int i=1;i<=m;i++)f[last][i]=-1e9;
	for(int i=1;i<=n;i++){
		for(int j=0;j<=m;j++)
			f[cur][j]=f[last][j];
		for(int j=0;j<=m;j++){
			if(j+a[i]<=m)f[cur][j+a[i]]=max(f[cur][j+a[i]],f[last][j]+a[i]);
			if(j<a[i])f[cur][a[i]-j]=max(f[cur][a[i]-j],f[last][j]-j+a[i]);
			else f[cur][j-a[i]]=max(f[cur][j-a[i]],f[last][j]);
		}
		last^=1;
		cur^=1;
	}
	cout<<f[last][0];
	return 0;
}

得分:\(320 pts\)

Day4(数据结构)

T1

出现次数
我们先把数组反转方便计算,用前缀和的思路,如果 \(a_i\) 没出现过, \(ans_i = ans_{i - 1} + 1\),否则 \(ans_i = ans_{i-1}\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
int a[100005],t[100005];
bool use[100005];
signed main(){
	int n,q;
	cin>>n>>q;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	reverse(a+1,a+n+1);
	for(int i=1;i<=n;i++){
	    if(!use[a[i]])t[i]=t[i-1]+1;
	    else t[i]=t[i-1];
	    use[a[i]]=true;
	}
	while(q--){
		int l;
		cin>>l;
		cout<<t[n-l+1]<<'\n';
	}
	return 0;
}

T2

组模拟赛
用一个桶记录 \(a_i\) 出现的次数,遍历这个桶,如果全部出现过就更新答案,然后把桶里所有元素 \(-1\),赛事得了 \(80\) pts。其中有一个很大的剪枝,如果桶中有的元素个数为 \(0\),可以直接 break,即可通过。

#include<bits/stdc++.h>
#define int long long
using namespace std;
int a[100005];
int use[100005];
int ans[100005];
signed main(){
	int n,m;
	cin>>n>>m;
	bool f=true;int cnt=0;
	for(int i=1;i<=n;i++){
		f=true;
		cin>>a[i];
		use[a[i]]++;
		for(int j=1;j<=m;j++){
			if(use[j]==0){
			    f=false;
			    break;
			}
		}
		if(f){
			cnt++;
			ans[cnt]=i;
			for(int j=1;j<=m;j++)if(use[j])use[j]--;
		}
	}
	cout<<cnt<<'\n';
	for(int i=1;i<=cnt;i++)cout<<ans[i]<<' ';
	return 0;
}

T3

完成作业
赛时 爆搜 \(+\) 骗分 得了 $40分 $。

正解:
按照时间排序处理输入数据,如果⽬前的⽐之前的优,可以替换掉之前的。
⽤优先队列(小根堆)维护这个可反悔贪⼼。

#include<bits/stdc++.h>
using namespace std;
const int MAXN=100005;
pair<int,int>a[MAXN];
signed main(){
	int n;
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i].first>>a[i].second;
	sort(a+1,a+n+1);
	priority_queue<int,vector<int>,greater<int> >q;
	int now=0,las=0;
	int ans=0;
	for(int i=1;i<=n;i++){
		now+=a[i].first-las;
		las=a[i].first;
		if(now>0){
			now--;
			ans+=a[i].second;
			q.push(a[i].second);
		}
		else{
			if(q.top()<a[i].second){
				ans-=q.top();
				ans+=a[i].second;
				q.pop();
				q.push(a[i].second);
			}
		}
	}
	cout<<ans;
	return 0;
}

T4

涂刷油漆
(老师的思路)⽤ ST表/单调队列 处理出每种刷油漆的操作对应的⾼度。
那⼀个柱⼦能达到的⾼度就是覆盖它的操作的最⼤值,这同样可以⽤ ST表或单调队列处理。
最少需要⽤⼏次操作可以从左往右贪⼼,如果⽬前没达到最⼤值就找到最右的那个最⼤的操作。
代码:不会qwq。

讲课:

前缀和:

太简单了,不讲
直接用一个数组 \(sum\) 前缀求和,即 sum[i]=sum[i-1]+a[i],如果想求出区间 \([l,r]\) 的和,就是 sum[r]-sum[l-1]

差分:

线段树用 update 函数更新区间,然后单点查询输出(划掉);
正解:

#include<bits/stdc++.h>
using namespace std;
int n,a[1000005],sum[1000005];
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	int T;
	cin>>T;
	while(T--){
		int l,r,v;
		cin>>l>>r>>v;
		sum[l]+=v;
		sum[r+1]-=v;//计算差值
	}
	for(int i=1;i<=n;i++)
		sum[i]+=sum[i-1];//前缀和维护
	for(int i=1;i<=n;i++)
		cout<<a[i]+sum[i]<<' ';//差值+原值
	return 0;
}

单调栈:

就是一个栈,不过栈内元素保证单调性。即,栈内元素要么从小到大,要么从大到小。
而单调栈维护的就是一个数 前/后 第一个 大于/小于 他的数。

#include<bits/stdc++.h>
using namespace std;
int n,a[3000006],ans[3000006];
stack<int>s;
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	for(int i=n;i>=1;i--){//倒序枚举 
		while(!s.empty()&&a[s.top()]<=a[i])s.pop();//比它小的就删去 
		if(s.empty())ans[i]=0;//没有比它小的就是0 
		else ans[i]=s.top();//否则就是栈顶元素 
		s.push(i);//不要忘记把每个数放进栈中 
	}
	for(int i=1;i<=n;i++)//正序输出答案 
		cout<<a[i]<<'\n';
	return 0;
} 

高维前缀和与差分:
朴素方法:\(f_{i,j}=f_{i-1,j} + f_{i,j-1} - f_{i-1,j-1} + a_{i,j}\)
还可以算出每一维前缀和,拆分开进行计算。

链表:

用来解决区间覆盖问题,可以 \(O(1)\) 实现插入和删除。

栈&&队列:

很简单,用指针维护即可。
分别用于 dfs 和 bfs。
PS:递归的原理就是栈。
PS:deque(STL双向队列)的空间很大,见NOI2022 D1T1。

分类:

栈:

  • 单调栈
  • 笛卡尔树
  • 离线 RMQ

队列:

  • 队列和双端队列
  • 单调队列
  • 优先队列(也就是堆,一般为二叉堆)。

单调队列还可以优化DP。

倍增:

倍增算法
给你 \(N\) 个数,\(m\) 次询问,每次询问找到 \(l-r\) 这个区间的最大值

定义一个二维数组 \(f\)\(f_{i,j}\) 表示从 \(a_i\) 开始 \(2^j\) 个数的最大值

但这些数太多了,于是中间“砍一刀 ”,得出两边都是 \(2^{j-1}\)
\(10^5 \times 10^5\) 的二维数组肯定不行,它是 \(2^{20}\) 次方了,所以二维开 \(20\)
\(f_{i,0}=a_i\)

\(f_{i,j} = \text max(f_{i,k-1},f_{i+2^{j-1},j-1})\)

即:\(f_{i,0} = a_i\)
\(f_{i,j} = \text max(f_{i,j-1},f_{i+(1<<(j-1)),j-1})\)

做法:一个区间长为 \(len\),寻找两个为 \(2\) 的整数倍的数,使得这两个数相加大于 \(len\),让 \(j\) 的值分别等于这两个数。

ST表:

用倍增的思路解决 rmq 问题,预处理时间复杂度为 \(O(n logn)\) 单次查询复杂度为O(1)

#include<bits/stdc++.h>
using namespace std;
int f[(int)1e5+5][20]; 
int a[(int)1e6+5];
vector<int>x(1e6+10); 
//x[i]表示长度为i的区间 用两个长度为2^x[i]的区间能够覆盖 
int main(){
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	x[1]=0;
	for(int i=2;i<=n;i++)
		x[i]=x[i>>1]+1;
		// i=111时,x[111]=x[55]+1;1111
	for(int i=1;i<=n;i++)
		f[i][0]=a[i];
	for(int j=1;(1<<j)<=n;j++)
		for(int i=1;i+(1<<j)-1<=n;i++)
			f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
	for(int i=1;i<=m;i++){
		int l,r;
		cin>>l>>r;
		int len=r-l+1;
		int j=x[len];
		cout<<max(f[l][j],f[r-(1<<j)+1][j])<<'\n';
	}	
	return 0;
}

可以用来实现 LCA。

并查集:

\(n\) 个点,每个点都有一条指向自己的边。
添加从点 \(i\) 到点 \(j\) 的边,就把 \(i\) 指向自己的那条边分开,指向点 \(j\)
如果点 \(i\) 上指向自己的边已经分开过了,我们就把与点 \(i\) 联接的点的指向自己的边分开,指向点 \(j\)

int go(int p){//求点 p 沿着并查集箭头最后会递归走到哪里,也就是查找 p 所在集合的末端元素 
	if(to[p]==p)return p;//如果点 p 的下一个点为自己,说明末端元素此时为点 p,也就是指向了自己 
	else return go(to[p]);//更新点 p 为点 p 的下一个点,继续递归调用沿着箭头寻找。
} 

Day5(图论)

T1

直接递归求解。
我们不停地让x,y中较大的那个往上走一步。
这样它们一定会在它们的lca处相遇
统计行走过程中的步数即可。
代码:太简单不写了。

T2

\(80\) 分解:dfs爆搜+骗分
\(100\) 分解:树上dp:
我们考虑一个简单的容斥
我们记录 \(f_{i,j}\) 表示在i的字数内距离i为j的点的个数
记录x以及他的父亲以及父亲的父亲形成的序列是 \(a_1 ,a_2\).....
其中 \(a_1=x\)
那么对于询问 \((x,k)\) 答案是
\(f_{a_{1},k} + f_{a_{2},k-1} - f_{a_{2},k-2} + f_{a_{3},k-2} - f_{a_{2},k-3}\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=5e5+10;
int nxt[MAXN*2],head[MAXN],to[MAXN*2],cnt;
int dis[MAXN][60],fath[MAXN];
int ans=0;
int k;
void add_edge(int a,int b){
	nxt[++cnt]=head[a];
    head[a]=cnt;
    to[cnt]=b;
}
void dfs(int x){
    dis[x][0]=1;
    for(int i=head[x];i;i=nxt[i]){
        if(to[i]==fath[x])continue;
        fath[to[i]]=x;
        dfs(to[i]);
        for(int j=1;j<=50;j++)
            dis[x][j]+=dis[to[i]][j-1];
    }
}
int getans(int x,int k){
    int ans=dis[x][k];
    k--;
    int i=x;
    for(;i!=1&&k;i=fath[i],k--)
        ans=ans+dis[fath[i]][k]-dis[i][k-1];
    if(i!=1&&k==0)ans++;
    return ans;
}
int pf[100005]; 
signed main(){
	//freopen("input.in","r",stdin);
	//freopen("output.out","w",stdout);
	int n,m;
	cin>>n>>m;
	for(int i=1;i<n;i++){
		int s,e;
		cin>>s>>e;
		add_edge(s,e);
		add_edge(e,s);
	}
    dfs(1);
	while(m--){
		int x,y;
		cin>>x>>y;
        cout<<getans(x,y)<<'\n';
	}
	return 0;
}

T3

首先顺序显然是可以传递的(这里需要吐槽ybw的误导)。
即有 \(12,23\) 时,代表着最后要存下的是 \(123\)
但是我们不用管,因为从另一方面,所有条件可以拆成若干 \(k_i=2\) 的条件。
也就是 \(x\) -> \(y\) 表示 \(x\) 必须比 \(y\) 先出现
这跟拓扑序是一样的
为了让字典序尽可能小,那就是要我们求最小字典的拓扑序
我们用一个堆维护所有当前可以加入答案序列的点,每次取出其中最小的并更新能加入答案序列的点,直到所有点都加入答案序列.
PS:直接用 set 存下数据后直接输出能得 \(20pts\)
正解:

#include <bits/stdc++.h>
#define MAXN 1000005
using namespace std;
int n,ans,cnt,s;
priority_queue<int>q;
int d[MAXN],head[MAXN];
bool in[MAXN];
struct edge{
	int to,next;
}e[maxn];
void add(int u,int v){
	e[++cnt].to=v;
	e[cnt].next=head[u];
	head[u]=cnt;
	d[v]++;
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		int x,last;
		cin>>x>>last;
		in[last]=true;
		for(int j=1;j<x;j++){
			int now;
			cin>>now;
			in[now]=true;
			add(last,now);
			last=now;
		}
	}
	for(int i=1;i<=1000000;i++)if((in[i])&&(d[i]==0))q.push(n-i);
	while(!q.empty()){
		int u=n-q.top();
        q.pop();
		for(int i=head[u];i;i=e[i].next){
			d[e[i].to]--;
			if(d[e[i].to]==0)q.push(n-e[i].to);
		}
		cout<<u<<' ';
	}
	return 0;
}

T4

\(100分\) 做法:注意到 \(a_i\) 很大,而 \(n\) 相对小
那么枚举 \(a_i \times k\) 的时候,很有可能有很多区间段里面一个点都没有
我们用 \(nxt_i\) 表示第一个比i大的数是哪个
我们每次通过 \(nxt_i\) 跳复杂度就是
复杂度在 \(n=10^5,m=10^7\) 需要跑 \(1亿\) 多次
在这种情况下复杂度大致是 \(O(m log (log n))\)
代码

Day5讲课:

树相关:

一般来说,以下定义说明了这一张图是一棵树:

  • 直接指明了这是一棵树。
  • 是一张 \(N\) 个点,\(N−1\) 条边的连通图。
  • 一张无环(视重边和自环都是环)的连通图。
  • 任意两点之间都有且仅一条简单路径。
  • ……

树相关的内容:树的遍历、直径、中心、重心。
树的储存一般用链表,特殊情况下用记录父亲或左儿子右兄弟的方式。

Day6(数学):

我太弱了!

T1

首先,我们预处理需要进位的情况。我们发现只有所有位都是 \(9\) 才会出现,直接将他 \(+1\)

然后思考解法:

输入一个整数 \(abcd\) ,若整数 \(abba\) 比输入的数大,则 \(abba\) 就是比它大的最小回文数,然后就可以直接输出答案
而如果 \(abba\) 小于等于输入的数,那我们就要找到比 \(abba\) 大的下一个回文数。
不难发现它就是:\(acca\)\(c=b+1\)

原理:一个回文数是根据它前半部分来的,所以它的下一个回文数就是原回文数的最中间的一个(或两个)数加1得来的。

#include<bits/stdc++.h>
using namespace std;
char s[205],ans[205];
signed main(){
    cin>>s;
    int len=strlen(s);
    for(int i=0;i<len&&s[i]=='9';i++){
        if(i==len-1){
            s[0]='1';
            len++;
            for(int j=1;j<len;j++)s[j]='0';
        }
    }
    for(int i=0;i<=len-i-1;i++)ans[i]=ans[len-i-1]=s[i];
    while(strcmp(ans,s)<=0){
        int x=(len+1)/2-1;
        while(ans[x--]=='9');
        x++;
        ans[x]=ans[len-x-1]=ans[x]+1;
        for(x++;x<=len-x-1;x++)ans[x]=ans[len-x-1]='0';
    }
    cout<<ans;
    return 0;
}

T2

把所有包含 \(2351257898\) 的数和它的倍数加入 vector 中,然后遍历求和。
题目保证答案在 long long 类型,数的倍数有 \(\text{floor}(\frac{10^{14}}{2351257898})\),含有 \(2351257898\) 的数,有 \(5 \times 10^{14-10}\),这是因为 \(2351257898\) 已经占了 \(10\) 个数位了。

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n;
const int QQ=2351257898;
vector<int>v,p(20);
signed main(){
	p[0]=1;
	for(int i=1;i<15;i++)
		p[i]=p[i-1]*10;
	cin>>n;
	for(int i=1;i<=n/QQ;i++)
		v.push_back(QQ*i);
	for(int i=1;i<=5;i++){
		for(int j=0;j<=9999;j++){
			int tmp=QQ*p[i-1]+j%p[i-1]+j/p[i-1]*p[i+9];
			if(tmp<=n)v.push_back(tmp);
		}
	}
	sort(v.begin(),v.end());
	v.erase(unique(v.begin(),v.end()),v.end());
	int ans=0;
	for(auto i:v)
		ans+=i;
	cout<<ans;
	return 0;
}

T3

看样例解释,我们发现了只会在 \(\text{floor}(\frac{x}{a_i})\) 的倍数处比前一个位置大 \(1\)

首先我们计算 \(f(k)\),然后开一个桶,\(桶_j\) 表示有多少个 \(a_i = j\),然后枚举,令 \(差分数组_j的倍数\) 加上 \(cnt\),复杂度为调和级数,\(O(n log n)\),开桶的意义是避免很
多个 \(a_i=1\) 导致时间复杂度变为 \(O(n^2)\)

#include<bits/stdc++.h>
#define MAXN 100010
#define int long long
using namespace std;
int a[MAXN],cnt[MAXN],n,l,r;
int ans[MAXN],tag[MAXN];
int pos;
signed main(){
	cin>>n>>l>>r;
	for(int i=1;i<=n;i++) 
		cin>>a[i];
	for(int i=1;i<=n;i++) 
		ans[0]+=l/a[i];
    for(int i=1;i<=n;i++) 
		if(a[i]<MAXN)cnt[a[i]]++;
    int s; 
	for(int i=1;i<=MAXN-1;i++){
        s=(l/i+1)*i;
        while(s<=r){
            tag[s-l]+=cnt[i];
            s+=i;
        }
    }
    for(int i=1;i<=n;i++){
        if(a[i]<MAXN)continue;
        s=(l/a[i]+1)*i;
        while(s<=r){
            tag[s-l]++;
            s+=a[i];
        }
    }
    for(int i=1;i<=r-l;i++)ans[i]=ans[i-1]+tag[i];
    for(int i=0;i<=r-l;i++)cout<<ans[i]<<' ';
	return 0;
}

T4

01 分数规划,没学。

Day6讲课

与、或、异或

C++中对应的运算符分别是 & | ^

二进制下:

\(1\) & \(1=1\)

\(1\) & \(0=0\)

\(0\) & \(0=0\)

\(1\) | \(1=1\)

\(1\) | \(0=1\)

\(0\) \(0=0\)

\(1 \oplus 1=0\)

\(1 \oplus 0=1\)

\(0 \oplus 0=0\)

通俗亿点:

& and与运算 二进制下相同位数的两个数都是1就得1,否则得0

| or或运算 二进制下相同位数的两个数至少有一个为1就得1,否则得0

^ xor异或运算 二进制下相同位数的两个数有且只有一个为1就得1,否则得0。

例题

[洛谷]数列之异或

//其实我是P党起家~
var n,ans:int64;
begin
    read(n);
    if (n and 1=0) then begin
        inc(n);
        ans:=n;
    end;
    if (((n+1)div 2) and 1=1) then begin
        ans:=ans xor 1;
    end;
    write(ans);
end.

分解质因数:

如果一个数的数位之和为 \(3\) 的倍数,那么这个数就是 \(3\) 的倍数。
如果一个数的个数位上为 \(0\)\(5\),那么这个数就是 \(5\) 的倍数。
如果一个数的千位及其以前的数位组成的数减去后三位数,得到的答案的绝对值为 \(7\)\(11\)\(13\) 的倍数,那么这个数就是 \(7\)\(11\)\(13\) 的倍数。

\(\text gcd\)\(\text lcm\)

辗转相除法(欧几里得算法)
分解质因子,min-max

组合数:

性质见数学篇:
组合数递推式:

	//C(i,j) 0<=i<=n,0<=j<=i 要求这个范围的组合数
	for(int i=0;i<=n;i++){
		C[i][0]=C[i][i]=1;
		for(int j=1;j<i;j++)
			C[i][j]=C[i-1][j-1]+C[i-1][j];
	}

Day7(全真模拟)

我太惨了!

T1

朴素的模拟,无脑输出。

#include<bits/stdc++.h>
using namespace std;
signed main(){
    int n;
    cin>>n;
    for(int i=1;i<=10*n+3;i++)putchar('#');
    putchar('\n');
    for(int i=1;i<=6*n+1;i++){
        putchar('#');
        if(i<=n){
            for(int j=1;j<=10*n+1;j++)putchar('.');
            putchar('#');
            putchar('\n');
            continue;
        }
        for(int j=1;j<=2*n;j++)putchar('.');
        if(i!=1&&i!=6*n+1&&i<4*n+n+1){
            putchar('#');
            for(int j=1;j<=8*n;j++)putchar('.');
            putchar('#');
            putchar('\n');
            continue;
        }
        if(i==4*n+n+1){
            for(int j=1;j<=6*n+1;j++)
                putchar('#');
            for(int j=1;j<=2*n;j++)putchar('.');
            putchar('#');
            putchar('\n');
            continue;
        }
        if(i<=6*n+2){
            for(int j=1;j<=8*n+1;j++)putchar('.');
            putchar('#');
            putchar('\n');
            continue;
        }
    }
    for(int i=1;i<=10*n+3;i++)putchar('#');
    return 0;
}

T2

首先枚举得到实际需要买多少个苹果,记为 \(m\)
我们将 \(c_3\)\(3 \times c1\)\(\text min\) ,将 \(c5\)\(5 \times c1\)\(\text min\),防止出现捆绑购买比单买更贵的情况。
然后我们枚举买多少组 \(5\) 个一组的,剩下的苹果尽量买 \(3\) 个一组的,再剩下的只能单买。

#include<bits/stdc++.h>
#define int long long
using namespace std;
signed main(){
	int n,c1,c3,c5;
	cin>>n;
	cin>>c1>>c3>>c5;
	c3=min(3*c1,c3);
	c5=min(5*c1,c5);
	int m=0;
	while(m+m/5*2+(m%5)/3<n)m++;
	int ans=m*c1;
	for(int i=0;i<=m;i++){
		int j=max(m-i*5,0ll);
		ans=min(ans,(int)i*c5+j/3*c3+min(c3,j%3*c1));
	}
	cout<<ans;
	return 0;
}

T3

赛时我直接把文章暴力展开,得了 \(40分\)

但发现可以不把文章展开,我们把文章当做仅由单词组成。设文章中第 i 个单词的编号为 \(a_i\)。我们求出 \(s_{a_i}\) 的长度的前缀和数组 \(sum_i\),即为文章中第 \(i\) 个单词的最后一个字符的位置。
询问时,我们可以在 \(sum\) 数组上二分得到查询位置对应的单词,然后算出对应哪个字符即可。

#include<bits/stdc++.h>
#define int long long
using namespace std;
string s[1000030],t;
int id[1000030];
int sum[1000030];
signed main(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        cin>>s[i];
    for(int i=1;i<=26;i++)
        s[n+i]='a'+i-1;
    cin>>t;
    int l=t.size();
    int now=0;
    int cnt=0;
    for(int i=0;i<l;i++){
        if('a'<=t[i]&&t[i]<='z')
            id[++cnt]=t[i]-'a'+1+n;
        else if(t[i]=='[')
            now=0;
        else if(t[i]==']')
            id[++cnt]=now;
        else now=now*10+t[i]-'0';
    }
    for(int i=1;i<=cnt;i++)
        sum[i]=sum[i-1]+s[id[i]].size();
    string ans;
    for(int i=1;i<=m;i++){
        int x;
        cin>>x;
        int p=lower_bound(sum+1,sum+cnt+1,x)-sum;
        if(p>cnt)ans+='!';
        else ans+=s[id[p]][x-sum[p-1]-1];
    }
    cout<<ans;
    return 0;
}

T4

BFS

今天的课程都是在讲前几年的真题。

习题

今日题单

几乎是 CSP-J 2019 ~ 2022 的所有题,除了一些过水题()

posted @ 2023-07-24 18:02  FinderHT  阅读(35)  评论(0编辑  收藏  举报