暑假模拟赛二 解题报告
唐山一中模拟赛一 解题报告
打这场比赛的时候分心很多,基本上就是 T1 一眼了一下然后实现,然后就开始死摆。一会儿摸鱼一会儿躺着,最后的将近 3 个小时都在摆的过程中偶尔推一下 T2。但显然 T2 打出了正解,但是由于一步小小的错误,加之结论出现了一点偏差,最后喜提 10 分。由于有 T1 的加持,吃相不算难看。感觉哪怕是在打暴力的角度上来讲,也是有很大提升的。比赛态度应该端正。
T1 湮灭反应 - \(100 \ pts\)
主要的分数来源。
看见连续段自然而然地想到两个方面:线段树 和 前缀和。笔者比较熟悉线段树,所以最先考虑了这个方向,发现没有办法查询绝对值最小区间和。所以果断调转矛头,考虑前缀和。不难发现,如果将前缀和排序,和的绝对值最小的一段左右两个端点的前缀和在排序后的数列里必然 连续。
因为在打 At 的时候被类似的恶心到过,很自然地就想到了这里,后面就非常简单了。std::multiset
一波存点遍历即可。当然其实本题直接 std::sort
也是可以的。
[ABC308G] Minimum Xor Pair Query
题意:带修,求最小异或对。
解法:注意到最小异或对的两个数在排序后的数列里必然连续,直接扔进一个
std::set
处理即可。
// Author: MichaelWong
// Code: C++14(GCC 9)
// Date: 2023/8/25
// File: annihilation.cpp
#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define pii std::pair<ll,ll>
#define fsp(x) std::fixed<<std::setprecision(x)
#define forE(u) for(int p=head[u],v=to[p];p;p=next[p],v=to[p])
const int N=1e5+5;
std::multiset<pii> s;
ll n,a[N],ps[N],ans=0x7fffffffffffffff,len=-1;
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
std::cin>>n; s.insert({0,0});
for(int i=1;i<=n;++i) std::cin>>a[i],ps[i]=ps[i-1]+a[i],s.insert({ps[i],i});
for(auto p1=s.begin(),p2=++s.begin();p2!=s.end();++p1,++p2) {
if(ans) {
if(p2->first-p1->first<ans) ans=p2->first-p1->first,len=std::abs(p2->second-p1->second);
else if(p2->first-p1->first==ans) len=std::max(len,std::abs(p2->second-p1->second));
}
if(p2->first==p1->first) {
while(p2->first==p1->first) ++p2; --p2;
len=ans?std::abs(p2->second-p1->second):std::max(len,std::abs(p2->second-p1->second));
ans=0,p1=p2,--p1;
}
}
if(!n) std::cout<<0<<' '<<0<<'\n';
else std::cout<<ans<<'\n'<<len<<'\n';
return 0;
}
// The code was submitted on Luogu for the contest on 8.25.
// Version: 1.
// If I filled in nothing on the statement,
// it means I'm in a contest and I have no time to do this job.
T2 树的计数 - \(10 \ pts\)
这道题是真的冤。本来是想出了正解,但是结论有一处反了,但这样也有 \(60 \ pts\)。但是我为了加速加上减少一点迭代深度,直接上了一个人类智慧表:
std::string sheet[5][20]={
{},
{"","X"},
{"","X(X)","(X)X"},
{"","X(X(X))","X((X)X)","(X)X(X)","(X(X))X","(((X)X)X)"},
{"X(X(X(X)))","X(X((X)X))","X((X)X(X))","X((X(X))X)","X(((X)X)X)","(X)X(X(X))","(X)X((X)X)","(X(X))X(X)","((X)X)X(X)","(X(X(X)))X","(X((X)X))X","((X)X(X))X","((X(X))X)X","(((X)X)X)X"}
};
你发现错误了吗?
对!sheet[4]
的第一个忘了加空 ""
!直接 \(60 \to 10\)。被评年度最 shaber 错误。
如果你打表打错了,那你还不如换一个逻辑性暴力。 —— Michael Wong
好的,接下来我们说正解。
来看一下每一个结点数的二叉树占的编号数量,
这样的数列显然必须有规律,否则这道题会变得非常难做。(对于笔者来说则是直接不可做。)所以我们找一下规律,发现这个就是 卡特兰数。
这样做有正确性吗?我们发现是有的。
- 卡特兰数的实际意义是 合法括号序列 的数量。我们可以把 二叉树形态 与 括号序列 进行一一对应的 映射,发现是可以做到的。比如 左儿子在括号外,右儿子在括号内,\(7\) 号二叉树就能表示为
()(())
。每一棵二叉树都能这样表示,对应到一个 唯一且合法的括号序列。这证明了他确实是卡特兰数。 - 我们发现,卡特兰数的通项公式也可以用二叉数解释:\(C_i\) 是一棵有 \(i\) 个结点的二叉树,\(C_j\) 表示左子树有 \(j\) 个点,\(C_{i-j-1}\) 表示右子树有 \(i-j-1\) 个点。
所以,我们就可以通过卡特兰数来解这道题了。我们先通过计算卡特兰数列前缀和,得到这个编号对应的二叉树的结点数,然后我们实现一个函数 \(\operatorname{solve} (num,ord)\),表示输出有 \(num\) 个结点的二叉树第 \(ord\) 个方案数。
我们考虑这个编号有什么意义。编号是按照左子树结点数 从小到大 记录的,这显然就是卡特兰数通项公式里面 \(j\) 的遍历顺序!所以我们模拟求解卡特兰数第 \(num\) 位的过程,如果有一个 \(k\) 使得 \(\sum_{j=0}^{k-1} C_j \cdot C_{num-j-1} < ord\) 而 \(\sum_{j=0}^{k} C_j \cdot C_{num-j-1} \geq ord\),那么我们可以断定左子树的大小是 \(k\)。那么,如何求解左子树是 \(k\) 点二叉树的第几个方案呢?
我们考虑 \(C_j \cdot C_{num-j-1}\) 的实际意义,这其实是在把 \(j\) 点二叉树的方案与 \(num-j-1\) 点二叉树的方案进行 一一搭配。在排序上,我们要让左子树编号小的二叉树编号更小,所以其实这形成了一个 横轴是 \(C_{num-j-1}\),纵轴是 \(C_j\) 的方案矩阵。方案数在这个矩阵上 从左至右,从上到下 排列。我们只需要求出这个方案数在矩阵上的坐标,就可以知道他的左右子树编号了。(笔者当时就是横纵轴弄反了……)
// Author: MichaelWong
// Code: C++14(GCC 9)
// Date: 2023/8/25
// File: treecnt.cpp
#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define pii std::pair<int,int>
#define fsp(x) std::fixed<<std::setprecision(x)
#define forE(u) for(int p=head[u],v=to[p];p;p=next[p],v=to[p])
const int N=5005;
int n,tot,C[N];
std::string sheet[5][20]={
{},
{"","X"},
{"","X(X)","(X)X"},
{"","X(X(X))","X((X)X)","(X)X(X)","(X(X))X","(((X)X)X)"},
{"","X(X(X(X)))","X(X((X)X))","X((X)X(X))","X((X(X))X)","X(((X)X)X)","(X)X(X(X))","(X)X((X)X)","(X(X))X(X)","((X)X)X(X)","(X(X(X)))X","(X((X)X))X","((X)X(X))X","((X(X))X)X","(((X)X)X)X"}
};
void analyze(int num,int ord) {
if(num<=4) return std::cout<<sheet[num][ord],void();
for(int i=0;i<num;++i) {
if(ord<=C[i]*C[num-i-1]) {
if(i) std::cout<<"(",analyze(i,(ord-1)/C[num-i-1]+1),std::cout<<")";
std::cout<<"X";
if(num-i-1) std::cout<<"(",analyze(num-i-1,(ord-1)%C[num-i-1]+1),std::cout<<")";
break;
}
ord-=C[i]*C[num-i-1];
}
}
signed main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
std::cin>>n; C[0]=1;
while(n) {
++tot;
for(int i=0;i<tot;++i) C[tot]+=C[i]*C[tot-i-1];
if(n<=C[tot]) break;
n-=C[tot];
}
analyze(tot,n);
return 0;
}
// The code was submitted on Luogu for the contest on 8.25.
// Version: 1.
// If I filled in nothing on the statement,
// it means I'm in a contest and I have no time to do this job.
T3 我没有说谎 - \(0 \ pts\)
非常不应该。这证明我又不会 二进制枚举 了……我已经连续 \(3\) 个二进制枚举暴力没有写了。
\(20 \ pts\) 做法:二进制枚举谁在说谎,判定结果即可。
难点就难在判定上。将 “\(a\) 个人分数比我高,\(b\) 个人分数比我低 ” 转化成 “\([b+1,n-a]\) 这段区间分数相同 ”。判定区间是否有交即可。
由此,我们想到可以施展 DP。\(dp_i\) 表示 \([1,i]\) 最多选取几个不交区间。枚举不要全扫一遍,用一个 std::vector
来记录有多少个区间右端点为当前点,枚举这些区间的左端点即可。
真没想到是 DP……我觉得还是应该想明白判定方法。转移到计算不交区间,赢面就很大了。
// Author: MichaelWong
// Code: C++14(GCC 9)
// Date: 2023/8/26
// File: nolying.cpp
// Human Intelligence!
#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define pii std::pair<int,int>
#define fsp(x) std::fixed<<std::setprecision(x)
#define forE(u) for(int p=head[u],v=to[p];p;p=next[p],v=to[p])
const int N=1e5+5;
int n,dp[N];
std::vector<int> lrec[N];
std::map<pii,int> cnt;
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
std::cin>>n;
for(int i=1,a,b;i<=n;++i) {
std::cin>>a>>b;
int r=n-a,l=b+1;
if(l>r) continue;
lrec[r].push_back(l);
cnt[{l,r}]<r-l+1&&(++cnt[{l,r}]);
}
for(int r=1;r<=n;++r) {
dp[r]=dp[r-1];
for(int l:lrec[r]) dp[r]=std::max(dp[r],dp[l-1]+cnt[{l,r}]);
}
std::cout<<n-dp[n]<<'\n';
return 0;
}
// The code was submitted on Luogu.
// Version: 1.
// If I filled in nothing on the statement,
// it means I'm in a contest and I have no time to do this job.
T4 美食家 - \(0 \ pts\)
……
感觉最多想到链表,\(40 \ pts\) 做法。他的这个双指针笔者真的是看了半天才明白,而且他的二分,是 BIT 上二分。
// Author: MichaelWong
// Code: C++14(GCC 9)
// Date: 2023/8/26
// File: foodie.cpp
#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define pii std::pair<int,int>
#define fsp(x) std::fixed<<std::setprecision(x)
#define forE(u) for(int p=head[u],v=to[p];p;p=next[p],v=to[p])
const int N=1e5+5;
int n,m,x,w[N],id[N],t[N],sum[N],BLN;
inline int lowbit(int x) { return x&-x; }
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr); std::cout.tie(nullptr);
std::cin>>n>>m>>x; ++x,BLN=1;
while(BLN<<1<=n) BLN<<=1;
for(int i=1;i<=n;++i) std::cin>>w[i];
for(int i=1,a;i<=n;++i) {
std::cin>>a; id[i]=i;
if(w[i]<x) t[i]=x-w[i],t[i]=t[i]/a+(bool)(t[i]%a);
if(t[i]) for(int j=i;j<=n;j+=lowbit(j)) ++sum[j];
}
std::sort(id+1,id+n+1,[](int a,int b){ return t[a]==t[b]?a<b:t[a]<t[b]; });
int rnd=1,pos=0,ans=0,ptr=1,tmp,las;
for(;ptr<=n;++ptr) if(t[id[ptr]]) break;
for(;ptr<=n;++ptr) {
while(rnd<=m) {
tmp=n-ptr+1;
for(int i=pos;i;i-=lowbit(i)) tmp-=sum[i];
if(ans+tmp<t[id[ptr]]) ans+=tmp,++rnd,pos=0;
else break;
}
if(rnd>m) break;
las=t[id[ptr]]-ans;
for(int i=pos;i;i-=lowbit(i)) las+=sum[i];
pos=tmp=0;
for(int lb=BLN;lb;lb>>=1) {
pos+=lb;
if(pos>=n||tmp+sum[pos]>=las) pos-=lb;
else tmp+=sum[pos];
}
++pos;
ans=t[id[ptr]];
for(;t[id[ptr]]==t[id[ptr+1]];++ptr) for(int i=id[ptr];i<=n;i+=lowbit(i)) --sum[i];
for(int i=id[ptr];i<=n;i+=lowbit(i)) --sum[i];
}
std::cout<<ans<<'\n';
}
// The code was submitted on Luogu.
// Version: 1.
// If I filled in nothing on the statement,
// it means I'm in a contest and I have no time to do this job.
总结
总的来讲,暴力的分还是打的太少了,也看到了其实对 DP 和 双指针模拟 的认识还是有局限性的。关键的点在于把问题转化,变成一个比较格式化的问题。(虽然感觉 T4 代码也不怎么是人写的。)