蒟蒻 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++}}\)