蒟蒻 OIerのCSP - J 膜你赛 2023 题解

蒟蒻 OIerのCSP - J 膜你赛 2023 题解

背景

T1 是之前精英组学长们的梗,拿来诈骗。

T2 是氰化某次考试时的原题,故事是这样的:

  • 我赛时写了一个吊打 std 的做法(就是题解里的做法)(std 多一个 \(\log\)),于是把带 \(\log\) 的做法放进了部分分,但是由于上传数据大小限制,可能卡不掉带 \(\log\) 的做法,我也无能为力。至今仍不会那种做法。
  • 当时我在交之前把题解中代码的 a 数组改成了原来的三分之一(即 \(a,b,c\) 的范围而非 \(a+b+c\) 的范围),但是其实题目中 \(a,b,c\) 是需要加起来的,于是痛失 \(90\) 分,从正数第二掉到倒数第三。

T3 是 SYZ 推荐的一道题目,同时感谢 SYZ 提供标程和题解。

T4 是我出的一道 NP(不能在多项式时间复杂度内解决的问题),在 AcWing 和清北群里与大佬讨论后优化掉了一个 \(n\),才有了现在题解中的做法,感谢他们的贡献。

另:本人没玩过原神。

#1.登陆

任务一

直接从 \(n\) 乘到 \(n-m+1\) 就行。时间复杂度 \(O(m)\)注意 long long 是不够的,我在造数据时才发现。

任务二

注意到前缀和的差分就是原数组,题目变成了一个差分板子。别告诉我你不会差分,你猜我为啥要考,还是放 T1

这个任务 0 分(Sub #3#4 全 WA)原因:

多半是没从 \(0\) 开始。尤其注意读入和输出。

代码

#define ll __int128
ll t,n,m,k,l,r,c,a[1000010],cf[1000010],ans=1;
const ll mod=1000000007ll;
int main(){
	cin>>t;
	if(t==1){
		cin>>n>>m;
		for(ll i=n-m+1;i<=n;i++){
			ans*=i;
			ans%=mod;
		}
		cout<<ans;
	}
	else{
		cin>>n>>a[0];
		cf[0]=a[0];
		for(int i=1;i<n;i++){
			cin>>a[i];
			cf[i]=a[i]-a[i-1];
		}
		cin>>k;
		while(k--){
			cin>>l>>r>>c;
			cf[l]+=c;
			cf[r+1]-=c;
		}
		a[0]=cf[0];
		cout<<a[0]<<' ';
		for(int i=1;i<n;i++){
			a[i]=a[i-1]+cf[i];
			cout<<a[i]<<' ';
		}
	}
	return 0;
} 

#2.对战

注意到我们是每打一个人就可以换一次,那我们考虑对每个人量身定制,也就是打每个人最菜的两项,由于要求严格大于,所以此时我们总的等级要大于这个人最菜的两项的等级加 \(2\)

但是如何优化到 \(O(n)\) 级别呢?

注意到 \(a,b,c\) 的范围并不大,考虑开个桶(代码中数组 a)维护最菜的两项的等级加 \(2\) 的每个值对应的人数。

接着对这个桶求个前缀和,此时 a[i] 就代表 \(i\) 的总等级可以打掉 a[i] 个人。然后对于每个人的三项之和可以 \(O(1)\) 求出答案。总时间复杂度 \(O(n+9\times 10^6)\)

代码:

直接贴的当时的代码,码风可能稍有不同。

int a[9000010],s[3000010],mx,n,x,y,z,tsum;
int main(){
	cin>>n;
	for(int i=0;i<n;i++){
		cin>>x>>y>>z;
		s[i]=x+y+z;
		if(x>=y&&x>=z){
			a[y+z+2]++;
		}
		else if(y>=x&&y>=z){
			a[x+z+2]++;
		}
		else{
			a[x+y+2]++;
		}
	}
	for(int i=0;i<9000010;i++){
		tsum+=a[i];
		a[i]=tsum;
	}
	for(int i=0;i<n;i++){
		cout<<a[s[i]]<<endl;
	}
	return 0;
} 

#3.法阵

注意:

按照阴阳平衡的原则,法阵要保证放的冰元素与火元素的数量差不超过 \(1\)。而且,由于这是一个很强的法阵,所以对于每一个点 \(u\),所有与根节点的简单路径经过 \(u\) 的点(包括 \(u\) 自身)中,放的冰元素与火元素的数量差也不能超过 \(1\)

这句话的意思是,对于每一个点为根的子树(包括这个点),冰元素和火元素的数量相差不超过 \(1\)。这么写是因为我叫 lhx 大佬帮我写题面,他说这句话极具迷惑性,于是我只重新改了剧情,留下了他这句话。

题解 By SYZ 大佬 %%%

看到火元素和冰元素数量相差不超过 \(1\),很容易就能想到维护火元素数量和冰元素数量之差。那么我们设计一个 dp:

\(f_{u,0/1/2}\) 表示只考虑 \(u\) 的子树,火元素数 \(-\) 冰元素数 \(=0,1,-1\) 的最小代价。

初始我们有 \(f_{u,1}=a_u,f_{u,2}=b_u\),也就是只考虑 \(u\) 本身。

我们还可以注意到一点:如果 \(u\)\(siz\) 为偶数,那么只有 \(f_{u,0}\) 是有意义的;否则,只有 \(f_{u,1}\)\(f_{u,2}\) 有意义。

考虑如何合并儿子。先把 \(siz\) 为偶数的点扔掉,那么现在问题就转换成了:你有若干个元素,有 \(c,d\) 两种属性(对应了 \(f_{v,0}\)\(f_{v,1}\),也包括 \(a_u,b_u\))。现在你要使 \(x\) 个元素(\(x\) 为定值)选 \(c\),剩下的选 \(d\),求和的最小值。

我们可以假设每个数一开始都是 \(d\),那么变成 \(c\) 就需要 \(c-d\) 的代价。由于要最小化和,那么我们把代价从小到大排序,取出最小的 \(x\) 个即可。

一个常错点:排序时应使用局部定义的 vector,否则向下递归时会把原有信息覆盖。

显然 vector 的总大小是所以点度数和级别的,而我们知道度数和是 \(O(n)\) 的,所以 vector 的总大小也是 \(O(n)\) 的。

时间复杂度 \(O(n\log n)\)

代码:

#include<bits/stdc++.h>
#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/tree_policy.hpp>
#include<ext/pb_ds/hash_policy.hpp>
#define gt getchar
#define pt putchar
#define fst first
#define scd second
typedef long long ll;
const int N=1e6+5;
using namespace std;
using namespace __gnu_pbds;
typedef pair<int,int> pii;
inline bool __(char ch){return ch>=48&&ch<=57;}
template<class T> inline void read(T &x){
	x=0;bool sgn=0;char ch=gt();
	while(!__(ch)&&ch!=EOF) sgn|=(ch=='-'),ch=gt();
	while(__(ch)) x=(x<<1)+(x<<3)+(ch&15),ch=gt();
	if(sgn) x=-x;
}
template<class T,class ...T1> inline void read(T &x,T1 &...x1){
	read(x);
	read(x1...);
}
template<class T> inline void print(T x){
	static char st[70];short top=0;
	if(x<0) pt('-');
 	do{st[++top]=x>=0?(x%10+48):(-(x%10)+48),x/=10;}while(x);
    while(top) pt(st[top--]);
}
template<class T> inline void printsp(T x){
	print(x);
	putchar(' ');
}
template<class T> inline void println(T x){
	print(x);
	putchar('\n');
}
struct edge{
	int to,nxt;
}e[N<<1];
int head[N],cnt,n,a[N],b[N],siz[N];
ll f[N][3];
inline void add_edge(int f,int t){
	e[++cnt].to=t;
	e[cnt].nxt=head[f];
	head[f]=cnt;
}
inline void add_double(int f,int t){
	add_edge(f,t);
	add_edge(t,f);
}
void dfs(int u,int fa){
	siz[u]=1;
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa) continue;
		dfs(v,u);
		siz[u]+=siz[v];
	}
	f[u][0]=f[u][1]=f[u][2]=1e18;
	vector<ll>vec;
	vec.emplace_back(a[u]-b[u]);
	ll all=b[u];
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa) continue;
		if(siz[v]&1){
			all+=f[v][2];
			vec.emplace_back(f[v][1]-f[v][2]);
		}
		else all+=f[v][0];
	}
	sort(vec.begin(),vec.end());
	int m=(int)vec.size();
	if(siz[u]&1){
		ll sum=all;
		for(int i=0;i<m/2;++i) sum+=vec[i];
		f[u][2]=sum;
		f[u][1]=sum+vec[m/2];
	}else{
		ll sum=all;
		for(int i=0;i<m/2;++i) sum+=vec[i];
		f[u][0]=sum;
	}
}
signed main(){
	read(n);
	for(int i=1;i<=n;++i) read(a[i],b[i]);
	for(int u,v,i=1;i<n;++i){
		read(u,v);
		add_double(u,v);
	}
	dfs(1,0);
	println(min({f[1][0],f[1][1],f[1][2]}));
	return 0;
}

#4.套娃

一个规律:当一个题目中某(几)个数据的范围异常时,极有可能为题目的突破口。几个常见的例子:

  • 一般属性的值域都是 \(a_i \le 10^9\) 或者 \(|a_i| \le 10^9\) 或者更大,如果出现 \(1 \le a_i \le 10^6\) 之类的很可能就是针对值域做题或者开桶
  • 当个数、区间之类的东西达到 \(10^{15}\) 时,题目很可能是推逝子 \(O(1)\) 或者 \(O(\log n)\) 级别的做法。
  • 当个数、区间之类的东西的范围只有 \(10\) 的时候,考虑状压或者是指数级别搜索;只有 \(20\)\(30\) 的时候,考虑状压或者 Meet-in-the-middle;只有 \(100\)\(300\) 的时候,考虑学过的 \(O(n^3)\) 算法。
  • ……

注意到 \(k\) 的范围并不大,考虑对 \(k\) 状压。

\(dis_{i,S}\) 为从第 \(1\) 个点到第 \(i\) 个点,抽卡状态为 \(S\) 时(其中 \(S\) 的第 \(j\) 个二进制位为 \(1\) 表示第 \(j\) 个已经抽到了)的最短路。

从起点开始 BFS,但用的不是普通队列,而是优先队列(即堆)。此处我们将搜索状态(优先队列中的元素)按照已经走的距离的顺序排序,然后每次搜已经走过距离最小的状态。这样可以保证每个点的每个抽卡状态只能被搜到一次。由于我比较懒所以数据是纯 rand 可能有重边,因此为了保险我的程序中一个状态可能被搜多次。时间复杂度 \(O(n2^k)\)

#define ll long long
#define lf double
#define ld long double
using namespace std;
struct node{
	ll now,dis,zt;
};
bool operator <(node x,node y){
	return x.dis>y.dis;
}
ll n,m,k,u,v,w,c[2010];
vector<ll> a[2010],b[2010];
ll dis[2010][1<<11]; 
priority_queue<node> q;
int main(){
	cin>>n>>m>>k;
	for(int i=1;i<=n;i++){
		cin>>c[i];
	}
	for(int i=0;i<m;i++){
		cin>>u>>v>>w;
		a[u].push_back(v);
		b[u].push_back(w);
		a[v].push_back(u);
		b[v].push_back(w);
	}
	memset(dis,0x3f,sizeof(dis));
	q.push({1,0,1<<(c[1]-1)});
	while(!q.empty()){
		ll now=q.top().now;
		ll tmp=q.top().dis;
		ll zt=q.top().zt;
		q.pop();
		for(int i=0;i<a[now].size();i++){
			if(dis[a[now][i]][zt|(1<<(c[a[now][i]]-1))]>tmp+b[now][i]){
				dis[a[now][i]][zt|(1<<(c[a[now][i]]-1))]=tmp+b[now][i];
				q.push({a[now][i],tmp+b[now][i],zt|(1<<(c[a[now][i]]-1))});
			}
		}
	}
	cout<<dis[n][(1<<k)-1];
	return 0;
} 

结语

“旅行者,当你重新踏上旅途之后,一定要记得旅途本身的意义。提瓦特的飞鸟、诗歌和城邦,女皇、愚人和怪物,都是你旅途的一部分。终点并不意味着一切,在抵达终点之前,用你的眼睛多多观察这个世界吧。”——温迪

OI 的世界亦是如此,大家多多观察,多多学习,多多实践,相信大家一定能遇到更好的自己。

Round 2,我们再会!

\(\Huge\mathsf{\color{red}\colorbox{yellow}{CSP2023 rp++}}\)

posted @ 2023-10-05 12:03  蒟蒻OIer  阅读(109)  评论(0编辑  收藏  举报