20260530 模拟测 总结
已严肃被踩爆。
其实我感觉前五题都很简单的来着,最后一题确实难,但是为什么我 C 和 E 都挂分了甚至最后一题的分也没拿到呜呜呜。
什么原来最后一题暴力要开 __int128,我错了,我再也不在取模的情况下比较大小了 /ll
什么啊 \(2 \times 10^8\) 三秒都过不去,是洛谷机子太 fvv 了还是我的常数太 fvv 了??这样很不公平知道嘛(,算了我赛时反正也糖了。T^T
红橙黄黄绿绿,不难啊 /dk。
延续上次 ptyb 总结的传统,用
包裹的为反思 qwq。
A. 音乐节拍器
做水题是会令人心情愉悦的。
我们发现 \(n\) 和 \(T\) 啊什么的都很小,于是我们可以直接暴力开个桶记录每个时刻有多少种乐器在发出声音,然后枚举每种乐器 \(p_i\) 让所有 \(p_i k\) 都打上标记,最后扫一遍判断即可。
时间复杂度:\(O(nT)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 1e4+5;
int n,T,k,p[25],c[N],cnt;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int main(){
n=read(),T=read(),k=read();
for(int i=1;i<=n;i++){
p[i]=read();
for(int x=p[i];x<=T;x+=p[i])c[x]++;
}cnt=0;
for(int i=1;i<=T;i++)if(c[i]==k)cnt++;
cout<<cnt<<"\n";
return 0;
}
B. 连续打卡
模拟题(?),不太好写。
首先你可以用 string 把所有这些打卡时间存下来,排序去重,string 是按字典序排序的,而在这题中字典序排序就是按时间顺序排序,不需要自己写什么比较器。
最重要的就是判断两个日子是否相邻,存在三种情况:
- 年份不同:当且仅当为相邻年,且一个是最后一天(\(12\) 月 \(31\) 日)一个是第一天(\(1\) 月 \(1\) 日)时相邻。
- 月份不同:当且仅当为相邻月,且一个是该月最后一天(需考虑闰年对 \(2\) 月的影响)一个是第一天时相邻。
- 年份月份都相同:只需判断具体日期是否相邻即可。
有一些小细节写的时候要注意一下,其他还好,思路是简单的。
时间复杂度:\(O(n \log n)\),瓶颈在于排序。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 1e5+5;
struct node{int yy,mm,dd;}p[N];
/*
0123456789
YYYY-MM-DD
*/
int n,dp[N],Ans;
string a[N];
int day[]={0,31,28,31,30,31,30,31,31,30,31,30,31};
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
node To_node(string s){
node tmp={0,0,0};
for(int i=0;i<4;i++)
tmp.yy=tmp.yy*10+s[i]-'0';
for(int i=5;i<7;i++)
tmp.mm=tmp.mm*10+s[i]-'0';
for(int i=8;i<10;i++)
tmp.dd=tmp.dd*10+s[i]-'0';
return tmp;
}
int End(int yy,int mm){
if(mm!=2)return day[mm];
if((yy%4==0&&yy%100!=0)||(yy%400==0))return day[mm]+1;
else return day[mm];
}
int Is_(node x,node y){
if(x.yy!=y.yy){//年份不一致
if(y.yy>x.yy+1)return 0;//间隔太远
if(x.mm!=12||x.dd!=31)return 0;//不是最后一天
if(y.mm!=1||y.dd!=1)return 0;//不是第一天
return 1;//相邻
}
if(x.mm!=y.mm){//月份不一致
if(y.mm>x.mm+1)return 0;//间隔太远
if(x.dd!=End(x.yy,x.mm))return 0;//不是最后一天
if(y.dd!=1)return 0;//不是第一天
return 1;//相邻
}
return (x.dd+1==y.dd);//只差一天
}
int main(){
n=read();
for(int i=1;i<=n;i++)cin>>a[i];
sort(a+1,a+n+1);
n=unique(a+1,a+n+1)-a-1;
for(int i=1;i<=n;i++)p[i]=To_node(a[i]);
dp[1]=1,Ans=1;
for(int i=2;i<=n;i++)
dp[i]=Is_(p[i-1],p[i])*dp[i-1]+1,
Ans=max(Ans,dp[i]);
cout<<Ans<<"\n";
return 0;
}
C. 抓取卡牌
呵。\(100 \to 55\),这就是不看题的后果。
我们不应该假想出题人是善良的,事实上自己出题的时候都会故意埋一些坑吧。在这道题的数据范围里,有一个很小的小细节,就是 \(a_i\) 的下界为 \(0\),这意味着某些卡牌给了等于没给因为一张没有。
但我在做的时候没有去考虑这个问题,而是固执地认为每种卡牌都至少有一张,所以最开始初始化的时候没有判断该卡牌是否存在就全部塞进了队列。
真的好亏。。。\(45\) 分呐,我要是加上了这些分我就,呵呵,幻想什么呢,不过排名上移一位罢了。。。
真的挂的好惨哦。所以下次看数据范围一定要看仔细呐,在这种黄题挂细节分真的好难受的懂嘛(哭。
最直接的想法就是把所有可能的卡牌价值全部塞进一个数组里,从大到小排序后取前 \(X\) 项。
哈哈当然这样是不可行的,不爆炸就怪了,我们需要考虑优化。
对于一张卡牌,如果其已经取了 \(c\) 次,那么接下来可能被取的只有第 \((c+1)\) 次,还不需要考虑后面的值,所以可以最开始把所有存在的卡牌的第一次的情况塞进一个优先队列,然后每次取最大的,加进总价值后把这张卡牌的下一次塞进队列即可,可以证明这样一定能取到最优。
如此反复取 \(X\) 个值即可。
时间复杂度:\(O(X \log n)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 2e5+5;
struct node{LL val,yuan,k,up;};
bool operator < (const node &A , const node &B){
return A.val<B.val;
}
LL n,X,a[N],c[N],Ans;
priority_queue<node> q;
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int main(){
n=read(),X=read();Ans=0;
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1;i<=n;i++)c[i]=read();
for(int i=1;i<=n;i++)
if(c[i])q.push({a[i],a[i],1,c[i]});
while(X--){
LL now=q.top().val;
LL yuan=q.top().yuan;
LL k=q.top().k;
LL up=q.top().up;
q.pop();
Ans+=now;//累加权值
if(k<up)//还有个数
q.push({yuan/(k+1),yuan,k+1,up});//放入队列
}cout<<Ans<<"\n";
return 0;
}
D. 子树染色
挺简单的。
定义 \(dp_i\) 表示只考虑以 \(i\) 为根的子树的情况下,至少要给多少个节点进行染色,最终答案就是 \(dp_1\)。
树形 DP,我们从下往上推,对于一个 \(dp_u\),先向下遍历其所有儿子 \(v\),然后让 \(dp_u = \sum dp_v\) 即可。
……吗?当然不!\(u\) 自身如果是关键节点,那么对它的子树还有一个额外的限制,这个好办,在 DP 的同时(或者你前面再跑一次 DFS 求也行)求一个子树大小 \(sz_u\),让 \(dp_u = \max(\sum dp_v , \lceil \frac{sz_u}{2} \rceil)\) 即可。
可以证明这样转移得到的答案是最小的,也可以证明可以构造出一种个数合法的方案。
时间复杂度:\(O(n)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 1e5+5;
int n,m,sz[N],dp[N];
bool vis[N];
vector<int> g[N];
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
void DFS_mksz(int u,int fa){
sz[u]=1;
for(int v:g[u])if(v^fa)
DFS_mksz(v,u),sz[u]+=sz[v];
return;
}
void DFS_dp(int u,int fa){
int res=0;
for(int v:g[u])if(v^fa)
DFS_dp(v,u),res+=dp[v];
dp[u]=res;
if(vis[u])dp[u]=max(dp[u],(sz[u]+1)/2);
return;
}
int main(){
n=read(),m=read();
for(int i=1;i<=m;i++){
int x=read();vis[x]=1;
}
for(int i=1;i<n;i++){
int x=read(),y=read();
g[x].pb(y),g[y].pb(x);
}
DFS_mksz(1,0);
DFS_dp(1,0);
cout<<dp[1]<<"\n";
return 0;
}
E. 勇闯迷宫
我的做法的时间复杂度里怎么 \(P\) 还带平方啊??
呵呵,赛时脑残了。
其实从最简单的角度思考,如果你要改变当前的环境状态,肯定是改成当前格子的状态是吧;但是有没有一种可能,你现在不改成当前格子状态,但到了后面会起效果,也是值的呢?
哈哈,我就是想复杂了,然后在考虑改变状态情况时又多套了一维 \(P\),于是我的时间复杂度就直接原地爆炸了。
其实根本没必要啊!你如果到了后面会起效果,你到了那个起效果的格子再改不就是了嘛,为什么要提前改呢?嗯哼,确实呀,所以确实是想复杂了,我的 Dijkstra 的时间复杂度应该是 \(O(nmP \log{nmP})\) 的……
怎么还是 T 了,\(95\) 分(赛后)?
我明明赛时知道 01-BFS 能做来着,为什么我要用更慢更劣的 Dijkstra 来做呢?不知道!!
以后还是采取自己能想到的最优做法吧……01-BFS 又不难写为啥不写它啊??
总结教训:
- 得到反常理的结论或方法时思考是否有必要。
- 在可采取的算法中选择最好写、最快速的。优先速度,但如果速度提升不大难写程度骤增的话还是可以选取稍微慢一些但更好写的做法的。
明显的分层图。
与以往不同的是,这里要分成 \(2P\) 个层,\(P\) 是环境状态,那个 \(2\) 表示当前用过了或还没用技能。
于是就可以做了,我们发现每次路径的权值只有 \(0\) 和 \(1\) 这两种,普通 BFS 不行但是 Dijkstra 又大材小用了,使用 01-BFS 解决。
时间复杂度:\(O(nmP)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 305;
struct node{
int x,y,val,is,p;
};
int n,m,P,Sx,Sy,Ex,Ey,a[N][N];
char c[N][N];
int dis[2][N][N][105],Ans;
int dx[]={1,0,-1,0,0},dy[]={0,1,0,-1,0};
//最后一栏的状态是【当前状态】
//下一次的状态要增加
//换句话说最开始在起点时状态为 P-1
deque<node> q;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
void BFS_qwq(){
memset(dis,0x3f,sizeof(dis));
while(!q.empty())q.pop_back();
dis[0][Sx][Sy][P-1]=0;
q.push_front({Sx,Sy,0,0,P-1});
while(!q.empty()){
int x=q.front().x,y=q.front().y;
int val=q.front().val;
int is=q.front().is,p=q.front().p;
q.pop_front();//弹出
for(int i=0;i<5;i++){//五种状态是因为还可以留在原地
int x2=x+dx[i],y2=y+dy[i];
if(x2<1||x2>n||y2<1||y2>m)continue;
if(c[x2][y2]=='#')continue;
int pp=(p+1)%P;//新状态
int nxval=val+(pp!=a[x2][y2]);//新的代价
if(dis[is][x2][y2][pp]>nxval){
dis[is][x2][y2][pp]=nxval;
if(pp==a[x2][y2])q.push_front({x2,y2,nxval,is,pp});//入队
else q.push_back({x2,y2,nxval,is,pp});//入队
}
if(!is){//可以考虑使用技能
int p2=a[x2][y2];
int nval=val+1;
if(dis[1][x2][y2][p2]>nval){
dis[1][x2][y2][p2]=nval;
q.push_back({x2,y2,nval,1,p2});//入队
}
}
}
}return;
}
int main(){
n=read(),m=read(),P=read();
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
cin>>c[i][j];
if(c[i][j]=='S')Sx=i,Sy=j;
if(c[i][j]=='T')Ex=i,Ey=j;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)a[i][j]=read();
BFS_qwq();
Ans=0x3f3f3f3f;
for(int i=0;i<P;i++)
Ans=min(Ans,dis[0][Ex][Ey][i]),
Ans=min(Ans,dis[1][Ex][Ey][i]);
cout<<(Ans==0x3f3f3f3f?-1:Ans)<<"\n";
return 0;
}
F. 足球训练
嗯这个的反思估计没那么长了。
首先我赛时进入了一个误区,就是题目要我们求得到底是最大的乘积取模后的值,还是最大的乘积取模后的值。(?)
是不是没听懂,没事你不需要听懂,意会一下怎么断句就能听懂了(。
题目要我们求的是最大的乘积,取模单纯只是一个为了输出的辅助而已,但取了模之后的值是不能作比较的。
第一个,打暴力,我忘了取模后的值不能作比较这个条件了,所以我是直接取模后去做 DFS 的,额好吧就 \(30\) 分没了只剩 \(15\) 了,说是要开__int128才能过,不管了,没试了。
第二个,正解那块,其实我离 \(60\) 分解很近了来着,但是没想到的一点就是你不需要实时把总和存下来,让所有 \(a'_i\) 增大以后做就可以了。
不过正解的二分确实没想到来着……挺巧,其实我当时列举做法的时候有 DP 有贪心有二分,但是没怎么往二分的方向想过啊。不过一些都是因为我没把 \(60\) 分的做出来!(愤怒
唉这个的反思怎么也这么长啊。
全场唯一有含金量,也是含金量最高的题!点赞!
但是真的只有绿吗我感觉评蓝不过分诶。
考虑当前总和为 \(S\) 的情况下,对于两个当前实力值分别为 \(x_1\) 和 \(x_2\) 的人,每次提升实力分别是加 \(y_1\) 和 \(y_2\) 时,具体让谁加更优。
为了方便,令 \(S = x_1 \times x_2 \times C\),其中 \(C\) 是剩下的人的实力值总乘积,在这里可以看做一个大常数。
让 \(x_1\) 加,就是 \(S' = (x_1 + y_1) \times x_2 \times C = \dfrac{x_1 + y_1}{x_1} \times S\);让 \(x_2\) 加,就是 \(S'' = x_1 \times (x_2 + y_2) \times C = \dfrac{x_2 + y_2}{x_2} \times S\)。比较 \(S'\) 和 \(S''\),当 \(S' > S''\) 时训练 \(x_1\),否则训练 \(x_2\)。
显然我们不需要真的将 \(S'\) 和 \(S''\) 两个值都算出来,只需要比较是否 \(\dfrac{x_1 + y_1}{x_1} > \dfrac{x_2 + y_2}{x_2}\),即 \(\dfrac{y_1}{x_1} > \dfrac{y_2}{x_2}\)(两边同减去 \(1\))。
此时你就收获了第一步的 \(60\) 分做法:由于这些 \(\dfrac{y}{x}\) 会随着 \(x\) 的增大(\(y\) 是不会变的)而减小,我们可以将其逐个扔进优先队列,每次取队头然后放入下一个对应的 \(\dfrac{y}{x'}\),以此类推取前 \(m\) 个让其具体 \(a\) 值增加,最后把乘积算出来即可。
这样就得到了一个 \(O(m \log n)\) 的做法,题目给的 \(m \le 10^9\),满分还是拿不到的,需要优化。
分母变化而分子不变是非常难搞的,而且每次这个分母的变化都是加上分子,倒过来也就是取倒数的话会很好做(每次 \(+1\)),而且再考虑倒数的时候还是可以比较大小的,只是原先要较大的现在要较小的。
\(m\) 这个量级非常提示我们有一个 \(\log\) 级别的因素在啊,其实每次取的 \(m\) 个值都是最小的 \(m\) 个,为了标出这 \(m\) 个值的具体区间,我们可以二分这 \(m\) 个值的最大值的最小值。或者,为了更明确,可以说成是二分取掉 \(m\) 个值后剩下的最小值的最大值。额好绕口。
如何 check 呢?很简单,我们只需要对每个 \(a_i\) 求出 \(\dfrac{a_i}{b_i}\)(也就是前面提到的所谓 \(\dfrac{x}{y}\)),看要多少个这样形式的值才能提升到 \(mid\),等价于要算出 \(\left \lceil (mid - \dfrac{a_i}{b_i}) \right \rceil\),这也就是个数,算出其总和判断是否 \(\le m\) 即可完成 check。实现的时候有一个细节就是说可能 \(\dfrac{a_i}{b_i} > mid\) 导致求出来的个数为负数,此时要和 \(0\) 取 \(\max\)(不然你真的取负数个嘛)。
由于这里是浮点二分,不能直接用什么 \(l < r\)、\(l \le r\) 作为二分判断,需要一个 \(eps = 10^{-7}\) 左右,然后二分终止判断为 \(r - l > eps\),即若 \(r - l \le eps\) 就当做其相等了(浮点数没有绝对的相等,因为存在精度误差)。
二分后得到了 \(res\),就可以知道每个 \(a_i\) 具体要加多少了,也就能对应得到最终的 \(a'_i\) 了。吗?
并不是这样的!因为可能存在相等情况,所以前面 check 函数的判断是 \(\le m\),也就是说可能到了最后还剩下一些 \(m\) 值没有使用完全。但,有一个非常好的性质,就是说此时一定 \(m < n\),否则所有 \(a_i\) 都能再拔高一截,求出来的 \(res\) 也就不正确了。因此只要你二分写对了,最后的 \(m\) 就很小,不会超过 \(n\),可以直接套用前面的 \(60\) 分做法(优先队列那个)以 \(O(n \log n)\) 解决。
最后算答案的时候记得取模哦。
总的时间复杂度是 \(O(n K + n \log n)\) 的,\(K\) 是浮点二分的次数,大致不超过 \(100\) 次。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 1e5+5;
const LL MOD = 998244353;
const LD eps = 1e-7;//浮点精度判断参考值
struct node{LL x,y,id;};
bool operator < (const node &A , const node &B){
//自定义比较器,比较 x/y 的大小(优先较小)
return A.x*B.y>B.x*A.y;//十字相乘避免误差
}
LL n,m,a[N],b[N],Ans;
LD l,r,res;
priority_queue<node> q;
//从小到大,用大根堆的话需将比较器反着写
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool check(LD p){
LL cnt=0;
for(int i=1;i<=n;i++){
LD t=(LD)(a[i])/(LD)(b[i]);
LL c=ceil(p-t);//个数
cnt+=max(0ll,c);
//个数可能为负,要和 0 取 max
if(cnt>m)return 0;
//不支持这么多次操作,p 值不合法
}return 1;//检测通过,p 值合法
}
int main(){
n=read(),m=read();
for(int i=1;i<=n;i++)
a[i]=read(),b[i]=read();
l=0,r=2e14,res=0;
int CC=100;
while(r-l>eps){
LD mid=(l+r)/2.0;
if(check(mid))res=mid,l=mid;
else r=mid;
}
//此时二分到了具体值 res
for(int i=1;i<=n;i++){
LD t=(LD)(a[i])/(LD)(b[i]);
LL c=ceil(res-t);//求出个数
c=max(0ll,c);//可能为负
a[i]+=c*b[i];//累加具体个数
m-=c;//总个数减少
q.push({a[i],b[i],i});//等待替补
}
while(m--){//还剩下一些不多的个数
auto [x,y,id]=q.top();q.pop();
a[id]+=b[id];//累加具体值
q.push({a[id],b[id],id});//等待替补
}
Ans=1;
for(int i=1;i<=n;i++)
Ans=Ans*a[i]%MOD;//最后算出乘积
cout<<Ans<<"\n";
return 0;
}
总结
贴几条教训,其他也没啥可多说的。
- 看清数据范围看清数据范围看清数据范围!!!
重要的事情说三遍 - 如果得到比较反常理的结论或方法时一定要思考是不是没必要!
- 在想到的、可采取的做法中选择最好写且最快速的!!!
- 取模后的值不能比较大小!!!

浙公网安备 33010602011771号