NOIP2020比赛题解

排水系统

1.高精度+拓扑排序模板(细节还挺多的)

2.这道题用到了int128(会爆long long),int128是到2^127,long long是到1e18(分母 2 最高 22 次,3 和 5 最高 11 次,所以会爆了)

3..首先应该实现分数的加法,这里注意要随时除 gcd 来保持分母分子互质。已知了流水的方式,那么我们可以直接在图中进行模拟,可以用 dfs / bfs 来模拟流水过程,即枚举每一个污水接受口排出的水。分析一下就可以发现排水的过程其实就是一个拓扑排序的过程,每到一个点就把它所有出边的点都更新一下就好了。

#include<bits/stdc++.h>
#define ll __int128
using namespace std;
const int N=1e5+10,M=5e5+10;
int n,m;
ll gcd(ll a,ll b){
	return b==0?a:gcd(b,a%b);
}
ll lcm(ll a,ll b){
	return a/gcd(a,b)*b;
}
struct node{
    ll p,q;
    node(){
        p=0,q=1;
    }
    node operator *(const ll &rhs) const{
        node res;
        res.p=p,res.q=q*rhs;
        ll g=gcd(res.p,res.q);
        res.p/=g,res.q/=g;
        return res;
    }
    node operator +(const node &rhs) const{
        node res;
        res.q=lcm(q, rhs.q);
        res.p+=p*(res.q/q);
        res.p+=rhs.p*(res.q/rhs.q);
        ll g=gcd(res.p,res.q);
        res.p/=g,res.q/=g;
        return res;
    }
}val[N];
struct Edge{
    int to, next;
}e[M];
int head[N],ecnt,ind[N],d[N];
void addedge(int from, int to){
    e[++ecnt]=(Edge){to,head[from]};
    head[from]=ecnt;
    ind[to]++;
}
queue<int> q;
vector<int> ans;
void toposort(){
    for(int i=1;i<=n;i++)
        if(ind[i] == 0) q.push(i);
    while(!q.empty()){
        const int x=q.front();
        q.pop();
        if(d[x]) val[x]=val[x]*d[x];
        for(int i=head[x];i;i=e[i].next){
            const int y=e[i].to;
            val[y]=val[y]+val[x];
            if(--ind[y]==0) q.push(y);
        }
    }
}
void print(ll n){
    if(n>9) print(n/10);
    putchar(n%10+48);
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        if(i<=m) val[i].p=1;
        scanf("%d",d+i);
        if(d[i]==0) ans.push_back(i);
        for(int j=1,v;j<=d[i];j++){
            scanf("%d",&v);
            addedge(i,v);
        }
    }
    toposort();
    for(int i=0;i<ans.size();i++){
		print(val[ans[i]].p);
		putchar(' ');
		print(val[ans[i]].q);
		putchar('\n');
	}
    return 0;
}

字符串匹配

1.本题要求 S=(AB)^iC 使得 A 中出现奇数次的字符数量 ≤ C 中出现奇数次的字符数量。

2.暴力思路:先枚举哪一段是 C,然后 hash 判前面循环节

#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
typedef long long ll;
const int N=1e6+10;
const ull base=130;
int t,n,c[26],cnt,sum[N][30];
ll ans;
string s;
ull h[N],power[N];
ull hs(int l,int r){
	return h[r]-h[l-1]*power[r-l+1];
}
void pre(){
	for(int i=1;i<=n;i++)
		h[i]=h[i-1]*base+s[i];
	for(int i=1;i<=n;i++){
		int x=s[i]-'a';
		c[x]++;
		if(c[x]%2) cnt++;
		else cnt--;
		for(int j=0;j<=26;j++)
			sum[i][j]=sum[i-1][j]+(cnt==j);
	}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=26;j++)
			sum[i][j]+=sum[i][j-1];
}
bool chk(int x,int len){
	return hs(1,x-len)==hs(len+1,x);
}
int main(){
	cin>>t;
	power[0]=1;
	for(int i=1;i<=N;i++) power[i]=power[i-1]*base;
	while(t--){
		memset(c,0,sizeof(c));
		memset(sum,0,sizeof(sum));
		cnt=0;
		cin>>s;
		n=s.size();
		s=' '+s;
		pre();
		memset(c,0,sizeof(c));
		cnt=0,ans=0;
		for(int i=n;i>=3;i--){
			int x=s[i]-'a';
			c[x]++;
			if(c[x]%2) cnt++;
			else cnt--;
			for(int len=1;len*len<i;len++){
				if((i-1)%len==0){
					if(chk(i-1,len)){
						ans+=sum[len-1][cnt];
					}
					if(len*len<i-1&&chk(i-1,(i-1)/len)){
						ans+=sum[(i-1)/len-1][cnt];
					}
				}
			}
		}
		cout<<ans<<endl;
	}
	return 0;
}

3.可以想到一种做法:把 (AB) 结合为一个字符串,判断 (AB)^i 是否是 SS 的前缀,并用借助预处理统计答案。

(AB)^i 判断:想到 KMP中的 next 数组。可以想到一个结论:字符串A的最短周期 =∣A∣−next[A]

从二开始枚举 i,只要 S∣(AB)^i∣​ 的最短周期是 ∣AB∣ 的约数,则 (AB)^i 一定是 S 的前缀。预处理出 next 数组即可

答案统计需要预处理出 S 每个后缀中出现奇数次字符数量即可确定 C 中出现奇数次的字符数量。统计答案同时更新到每一位时出现奇数次的字符数量 ≤x (x∈[0,26]) 的 A 的数量。

#include<bits/stdc++.h>
#define int long long
using namespace std;
typedef unsigned long long ull;
const ull base=131;
const int N=2e6+10;
int t,n,ans=0,vis[30],r[30],tot=0;
short a[N],b[N],c[N],sum[N],sum1[N];
ull v[N],l[N];
char s[N];
signed main(){
	scanf("%lld",&t);
	l[0]=1;
	for(int i=1;i<=N;i++) l[i]=l[i-1]*base;
	while(t--){
		scanf("%s",s+1);
		n=strlen(s+1);
		memset(a,0,sizeof(a));
		memset(b,0,sizeof(b));
		memset(r,0,sizeof(r));
		memset(vis,0,sizeof(vis));
		memset(sum1,0,sizeof(sum1));
		ans=0,tot=0;
		for(int i=1;i<=n;i++){
			c[i]=s[i]-'a'+1;
			r[c[i]]^=1;
			if(r[c[i]]) sum[i]=sum[i-1]+1;
			else sum[i]=sum[i-1]-1;
			a[i]=sum[i];
		}
		memset(r,0,sizeof(r));
		for(int i=n;i>=1;i--){
			r[c[i]]^=1;
			if(r[c[i]]) sum1[i]=sum1[i+1]+1;
			else sum1[i]=sum1[i+1]-1;
			b[i]=sum1[i];
		}
		for(int i=1;i<=n;i++) v[i]=base*v[i-1]+1ull*c[i];
		for(int i=2;i<=n;i++){
			for(int j=a[i-1];j<=26;j++) vis[j]++;
			for(int j=i;j<n&&v[i]==v[j]-v[j-i]*l[i];j+=i) ans+=vis[b[j+1]];
		}
		printf("%lld\n",ans);
	}
	return 0;
}

微信步数

1.题目挺难懂的哈(只会暴力)

2.称 n 步为一轮,首先 −1 的情况很好判断:一轮后回到原地且在第一轮里存在某个起点走不出去。把答案转换一下:原本是考虑每个起点各自走多少步出界,现在转换成同时考虑所有起点,把每天还 存活的起点 数量计入贡献。(这里存活就是指从该起点出发到某天还没出界)显然,只要把第 0 天活着的起点算进去(也就是 ∏wi​),就和要算的答案等价了。一共 m 个维度,每个维度存活的位置是独立的,并且应是一段区间(只有开头、结尾的一部分会死亡)。

如果第 j 维存活的区间是 [lj​,rj​],那总共存活的数量就为∏​ (rj​−lj​+1)。(j:1~m)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=5e5+10,mod=1e9+7;
int w[20],e[20],l[20],r[20];
int c[N],d[N];
int n,m;
int main(){
    scanf("%d%d",&n,&m);
    ll ans=1;
    for(int i=1;i<=m;i++){
        scanf("%d",&w[i]);
        ans=ans*w[i]%mod;
    }
    for(int i=1;i<=n;i++){
        scanf("%d%d",&c[i],&d[i]);
    }
    while(1){
        for(int i=1;i<=n;i++){
            e[c[i]]+=d[i];
            l[c[i]]=min(l[c[i]],e[c[i]]);
            r[c[i]]=max(r[c[i]],e[c[i]]);
            ll s=1;
            for(int j=1;j<=m;j++){
                if(r[j]-l[j]>=w[j]){
					printf("%lld\n",ans);
					return 0;
				}
               s=s*(w[j]-r[j]+l[j])%mod;
            }
            ans=(ans+s)%mod;
        }
        bool flag=1;
        for(int j=1;j<=m;j++){
            if(e[j]!=0) flag=0;
        }
        if(flag){
            ans=-1;
            break;
        }
    }
    printf("%lld\n",ans);
    return 0;
}

移球游戏

1.貌似是一道构造题?

2.用分治。每次假定只有两个颜色,达到的目的是每种颜色都在一边,接下来分两边递归处理。A 颜色和 B 颜色内部的顺序不用管,因为在接下来的递归中,它们都会被处理到。具体实现用一个 vector 处理就行了。

第一步,整理。即将每一列整理成上面 B 颜色,下面 A 颜色的状态。

第二步,我们使用合并操作。首先定义翻转操作。非常简单,将一列的数全部插入空列。

接着把两列的上面分别放进空集。把第二列的剩下的填满第一列。把空集里的东西丢回第二列。

这样每次都可以生成一个纯色列。结合这两大步,可以做完一次操作。

#include<bits/stdc++.h>
using namespace std;
const int N=60,M=410,K=820010;
int a[N][M],top[N],n,m,ans[K][2],tot;
bool flag[N];
inline void pour(int x,int y){
	ans[++tot][0]=x,ans[tot][1]=y;
	a[y][++top[y]]=a[x][top[x]--];
}
void solve(int l,int r){
	if(l==r) return;
	int mid=l+r>>1;
	memset(flag,0,sizeof(flag));
	for(int i=l;i<=mid;i++)
		for(int j=mid+1;j<=r;j++){
			if(flag[i] || flag[j]) continue;
			int s=0;for(int k=1;k<=m;k++) s+=(a[i][k]<=mid);
			for(int k=1;k<=m;k++) s+=(a[j][k]<=mid);
			if(s>=m){
				s=0;for(int k=1;k<=m;k++) s+=(a[i][k]<=mid);
				for(int k=1;k<=s;k++) pour(j,n+1);
				while(top[i]) a[i][top[i]]<=mid?pour(i,j):pour(i,n+1);
				for(int k=1;k<=s;k++) pour(j,i);
				for(int k=1;k<=m-s;k++) pour(n+1,i);
				for(int k=1;k<=m-s;k++) pour(j,n+1);
				for(int k=1;k<=m-s;k++) pour(i,j);
				while(top[n+1]){
					if(top[i]==m||a[n+1][top[n+1]]>mid) pour(n+1,j);
					else pour(n+1,i); 
				}
				flag[i]=1;
			}
			else{
				s=0;
				for(int k=1;k<=m;k++) s+=(a[j][k]>mid);
				for(int k=1;k<=s;k++) pour(i,n+1);
				while(top[j]) a[j][top[j]]>mid?pour(j,i):pour(j,n+1);
				for(int k=1;k<=s;k++) pour(i,j);
				for(int k=1;k<=m-s;k++) pour(n+1,j);
				for(int k=1;k<=m-s;k++) pour(i,n+1);
				for(int k=1;k<=m-s;k++) pour(j,i);
				while(top[n+1]){
					if(top[j]==m||a[n+1][top[n+1]]<=mid) pour(n+1,i);
					else pour(n+1,j); 
				}
				flag[j]=1;
			}
		}
	solve(l,mid),solve(mid+1,r);
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) 
		for(int j=1,x;j<=m;j++)
			scanf("%d",&x),a[i][++top[i]]=x;
	solve(1,n);
	printf("%d\n",tot);
	for(int i=1;i<=tot;i++) printf("%d %d\n",ans[i][0],ans[i][1]);
	return 0;
}

总之前两道题需要注意的细节挺多的,后两道题较难,题目也比较难理解,尽力打打暴力

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值