20260811 - 暑假马力赛 总结
前言
马力赛太恐怖了,害怕打磨你怎么办。事实上并没有出现打磨你
但是马力赛没有可可爱爱的线段树差评。还好赛前没打板子不然吃亏了
mmt:马力赛的代码长度也没有很长哈,最长的是 \(110\) 行。
:……?\(110\) 行很吓人了好吗?
*补完 F 题后
:不对啊,这最长的不也就七十多行吗,而且还算上了我十多行的define、代码中的神秘注释和调题时的记录,看来马力赛也不是很考验马力啊。
:最新消息:\(50 \times 50 = 250\),这是榜一大佬告诉我们的,这肯定是真的!
不敢笑,我真把 \(50 \times 50\) 算成 \(250\) 过,看来我确实是个 \(250\)。(点头
A - All Lengths Subtraction
水题,发现“美丽”的数组一定是两段升序(不要求严格)并且第一段升序的开头不能小于第二段升序的结尾。维护一下判断就行。
戳这里看代码
#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 = 105;
int T,n,p[N];
bool flag;
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(){
T=read();
while(T--){
n=read();
for(int i=1;i<=n;i++)p[i]=read();
flag=1;
for(int k=1;k<=n;k++){
int x=n-k+1;
int cnt=0,l=n+1,r=0;
for(int i=1;i<=n;i++)
if(p[i]==x)l=min(l,i),r=max(r,i),cnt++;
if(cnt!=k)flag=0;
if(r-l+1!=cnt)flag=0;
for(int i=1;i<=n;i++)
if(p[i]==x)p[i]--;
}
if(flag)cout<<"YES\n";
else cout<<"NO\n";
}
return 0;
}
- 时间复杂度:\(O(Tn^2)\)
- 空间复杂度:\(O(n)\)
B - Find it!
我认为是全场最简单的,同时也是我写的第一题。最小生成树板子,不过删负数边是不优的,不如保留下来。
戳这里看代码
#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 lines{LL x,y,z;}ln[N];
int n,m,fa[N];
LL ans;
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 cmp(lines l1,lines l2){return l1.z<l2.z;}
int FF(int u){return (fa[u]==u?u:fa[u]=FF(fa[u]));}
int main(){
n=read(),m=read();
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1;i<=m;i++)
ln[i].x=read(),ln[i].y=read(),ln[i].z=read(),
ans=max(ans,ans+ln[i].z);
sort(ln+1,ln+m+1,cmp);
for(int i=1;i<=m;i++){
int x=ln[i].x,y=ln[i].y;
x=FF(x),y=FF(y);
if(x!=y)fa[y]=x,ans-=max(0ll,ln[i].z);
}cout<<ans<<"\n";
return 0;
}
- 时间复杂度:\(O(\alpha(n)m\log m)\)
- 空间复杂度:\(O(n+m)\)
C - Following Directions S
某巨佬使用了 DSU,虽然不知道为什么要用但还是,膜拜。
令 \(dp_{i,j}\) 表示去往 \((i,j)\) 这个点的奶牛的数量,这个可以 \(O(n^2)\) DP 预处理出来,那么答案就是 \(\sum_{i=n+1 \text{or} j=n+1} c_{i,j} dp_{i,j}\)。然后修改的时候只有 \(dp_{x,y}\) 往下一路受到影响,先 \(O(n)\) 删去旧值,再 \(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 = 1505;
int n,c[N][N],dp[N][N],Q;
char a[N][N];
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();
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)cin>>a[i][j];
c[i][n+1]=read();
}
for(int i=1;i<=n;i++)c[n+1][i]=read();
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
dp[i][j]++;
if(a[i][j]=='R')dp[i][j+1]+=dp[i][j];
else dp[i+1][j]+=dp[i][j];
}
int ans=0;
for(int i=1;i<=n;i++)
ans+=c[i][n+1]*dp[i][n+1],
ans+=c[n+1][i]*dp[n+1][i];
cout<<ans<<"\n";
Q=read();
while(Q--){
int sx=read(),sy=read();
int cnt=dp[sx][sy];
int x=sx,y=sy;
while(x!=n+1||y!=n+1){
dp[x][y]-=cnt;
if(x==n+1||y==n+1)break;
if(a[x][y]=='R')y++;else x++;
}
a[sx][sy]=(a[sx][sy]=='R'?'D':'R');
x=sx,y=sy;
while(x!=n+1||y!=n+1){
dp[x][y]+=cnt;
if(x==n+1||y==n+1)break;
if(a[x][y]=='R')y++;else x++;
}
int res=0;
for(int i=1;i<=n;i++)
res+=c[i][n+1]*dp[i][n+1],
res+=c[n+1][i]*dp[n+1][i];
cout<<res<<"\n";
}
return 0;
}
/*
主要就是修改咋做
每次修改可以给你 O(n) 的时间跑一只奶牛的路径
翻转的位置就是罪魁祸首嘛
其他的地方都不会变的
路途中会经过这里的奶牛的个数需要统计一下吧
需要沿着原来的算一遍,减掉
再沿着新的算一遍,加上
最后算答案是可以扫一遍的所以核心在于 DP
*/
- 时间复杂度:\(O(n^2 + Qn)\)
- 空间复杂度:\(O(n^2)\)
D - Sorting Color Balls
没有发现是逆序对,倒闭。浪费 1h .jpg
发现整个过程类似于冒泡排序,那么考虑按顺序处理每个点,将其挪到最左边后视作离开数列,这样每次操作只需要算出当前操作的值左边有多少个值,以及左边有多少个和它同样颜色的值。前者可以用权值树状数组解决,后者可以在离散化后对每种颜色开个权值树状数组(用 vector 存)解决。至于处理的顺序就是将数列中元素升序排序后的顺序,然后如果有重复就贪心选最靠左的那个,可以证明不会变劣。
虽然但是这种做法说起来简单代码还是有亿点史的所以调了很久。用逆序对做会简单一些估计,但是我不喜欢写归并,树状数组那不还是一个道理啊,所以换算下来我也没吃亏。
戳这里看代码
#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 = 3e5+5;
int n,col[N],num[N],tmp[N];
int tr[N];
LL Ans;
int pos[N],len[N];
vector<int> s[N],t[N];
queue<int> q[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 upd(int x,int k){while(x<=n)tr[x]+=k,x+=x&-x;return;}
int ask(int x){int sum=0;while(x)sum+=tr[x],x-=x&-x;return sum;}
void change(int o,int x,int k){
while(x<=len[o])
t[o][x]+=k,x+=x&-x;
return;
}
int query(int o,int x){
int sum=0;
while(x)
sum+=t[o][x],x-=x&-x;
return sum;
}
int main(){
n=read();
for(int i=1;i<=n;i++)
s[i].pb(0),t[i].pb(0);//1-based
for(int i=1;i<=n;i++)
col[i]=read(),upd(i,1);//读入并加入大树状数组
for(int i=1;i<=n;i++){
pos[i]=s[col[i]].size();//记下标
s[col[i]].pb(i),t[col[i]].pb(0);//把值存进去
}
for(int i=1;i<=n;i++)
len[i]=s[i].size()-1;//树状数组大小(每种颜色的数量)
for(int i=1;i<=n;i++)
change(col[i],pos[i],1);//加入颜色专属树状数组
//cout<<pos[1]<<" "<<col[1]<<" , "<<pos[5]<<" "<<col[5]<<"!\n";
//cout<<query(col[1],pos[1])<<" "<<query(col[1],pos[5])<<"awa\n";
for(int i=1;i<=n;i++)
num[i]=read(),q[num[i]].push(i),tmp[i]=num[i];
sort(tmp+1,tmp+n+1);
for(int i=1;i<=n;i++){
int now=tmp[i];
int u=q[now].front();q[now].pop();
int cnt=ask(u-1);//查询前面还有多少个比自己小的
int del=query(col[u],pos[u]-1);//查询前面比自己小的中有多少个和自己相同颜色的
//cout<<u<<"!"<<cnt<<"-"<<del<<"!!!\n";
Ans+=cnt-del;//累加答案
upd(u,-1);//删掉自己
change(col[u],pos[u],-1);//删掉自己
}
cout<<Ans<<"\n";
return 0;
}
/*
发现整个过程就是一个 mppx
而 mppx 正常来做是 O(n^2) 的
而且 mppx 有稳定性
在这里我们也运用这个
因为这个东西不要求严格升序
所以对每个点找离自己最近的那个转移过来
我们从小到大考虑每个位置
然后找到后面第一个满足要求的值
把它整到这边来
然后顺便用一个权值树状数组算前面有多少个同种颜色的减掉
但是你还需要维护每个点的实时位置吧
实现区间加法单点查询可以用树状数组差分做
好那就没啥问题了……
并非吧这样一点也不好维护
多维护几层套在一起就乱了好像
对前面的再维护一棵权值是不是就行了
对还没动的和已经跑了的维护两棵权值这样
我试试看?毕竟树状数组好写。
不对啊我们并不需要维护这个当前位置啊。
因为只需要知道前面有多少个值就可以了。
你跑到最左边那不就是似掉了吗
但是这个怎么维护左边有多少个相同颜色呢??
难道动态+离散化+树状数组吗?
是不是可以用 set 啊??
*/
- 时间复杂度:\(O(n \log n)\)
- 空间复杂度:\(O(n)\)
E - Equal Frequencies
感谢 cmy dsa 仙人指路告诉我做完 C 之后应该先做 E 而不是 D,事实证明这是正确的。
发现每种字母的出现次数一定是 \(n\) 的因数,那就可以 \(\sqrt{n}\) 枚举因数做了。(ps A 掉这道题后才发现不需要枚举因数可以直接枚举用到了几个字母这样是 \(26\) 次近似常数会跑得更快)
然后重点在于怎么去构造是最优的,赛时卡了好一会。简单来说就是,令 \(x\) 表示每种出现了的字母的出现次数,那么就把出现次数前 \(\frac{n}{x}\) 多的字母作为最终答案,那么后面还剩下的部分就拿过来填坑;接着前面这些可能会有多出来的,多出来的就放到少一些的那里去填坑,总之总的来说就是一个填坑的操作。当然代码有点长需要注意细节,我实现这个部分的时候用了挺久的。
戳这里看代码
#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;
struct node{int c,id;}t[30];
int T,n,p[30],Ans,cnt;
string s;
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;
}
bool cmp(node d1,node d2){return d1.c>d2.c;}
int sol(int x){
int res=0;
if(n/x>26)return n+1;
for(int i=0;i<n/x;i++)
res+=max(0,t[i].c-x);
for(int i=n/x;i<26;i++)res+=t[i].c;
return res;
}
int main(){
T=read();
while(T--){
n=read();
cin>>s;s=" "+s;
Ans=n,cnt=n;
for(int i=0;i<26;i++)t[i]={0,i},p[i]=0;
for(int i=1;i<=n;i++)t[s[i]-'a'].c++,p[s[i]-'a']++;
sort(t,t+26,cmp);
for(int k=1;k*k<=n;k++){
if(n%k)continue;
int x=k,res=sol(x);
if(res<Ans)Ans=res,cnt=x;
x=n/k,res=sol(x);
if(res<Ans)Ans=res,cnt=x;
}
cout<<Ans<<"\n";
vector<int> vec;vec.clear();
for(int x=n/cnt;x<26;x++)
for(int i=1;i<=n;i++)
if(s[i]-'a'==t[x].id)vec.pb(i);
int now=n/cnt-1;
for(int i=0;i<vec.size();i++){
int x=vec[i];
while(t[now].c>=cnt)now--;
if(t[now].c<cnt)t[now].c++,s[x]=(char)('a'+t[now].id);
else{cout<<"WHAT?\n";return 0;}//按理来说不可能发生
}
for(int i=1;i<=n;i++)
if(p[s[i]-'a']>cnt){//超出,拿去填充
p[s[i]-'a']--;
while(t[now].c>=cnt)now--;
if(t[now].c<cnt)t[now].c++,s[i]=(char)('a'+t[now].id);
else{cout<<"WHAT?\n";return 0;}//按理来说不可能发生
}
for(int i=1;i<=n;i++)cout<<s[i];cout<<"\n";
}
return 0;
}
/*
把前 n/x 个提取出来
那么 x 个都是以它们为原型
后面剩下了 sheng 个怎么办呢?
先拿到前面去填充。
那么这些填充的部分就是需要增加的部分。
一定能够填充完不然就是写错了。
但这个时候会存在多的情况。
把多的那部分拿出来,这也是需要增加的部分。
加起来即可。
然后至于构造具体的方案这个就是按照上面那个步骤去做
不是很好做。
首先把
*/
- 时间复杂度:\(O(n C \sqrt{n})\),其中 \(C = 26\)(事实上可以做到 \(O(n C^2)\))
- 空间复杂度:\(O(n+C)\)
F - Two Currencies
那个那个我插句题外话哈,赛时被卡题了骗不到分怎么办!不用乱搞不用写暴力,如果是 AT 的题目,我们发现分数的计算方法是通过测试点数除以总测试点数四舍五入为两位小数,那么听好了,AT 的题的测试点是包含样例的,只要你做好足够的判断,输出样例就能骗到分,样例越多越好骗,记好了,输出样例!!!
至于我为啥会提这个是因为我在这题骗的分就是输出样例(,\(15\) 分嘞!
很可惜的,我在最后 20min 想到了正解,然后实现的时候跑偏了在那里枚举什么东西导致最终爆炸,唉,本来可以 AK 的,唉唉唉。
回到正题,我们发现这个东西要记录状态必须涉及到一个叫做当前剩余银币数的东西,但是这个是不是太大了,\(10^9\) 存都存不下来,但是 \(A\) 和 \(n\) 都很小,总共需要用到的银币数 \(An\) 也很小,那么就可以直接把 \(\ge An\) 的部分变成 \(An\) 统一考虑,分层图 Dijkstra 即可。
我里面做的是 \(\sum A\),其实都是没什么区别的哈。
戳这里看代码
#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 = 55;
const int K = 5005;
const LL INF = 4e18+5;
struct To{LL v,cst,tim;};
struct node{LL u,hav,step;};
bool operator < (const node &A , const node &B){
return A.step>B.step;
}
LL n,m,S,c[N],d[N],ans[N];
LL mxcst,dis[N][K];
vector<To> g[N];
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;
}
void Dijkstra(){
for(int i=1;i<=n;i++)
for(int j=0;j<=mxcst;j++)dis[i][j]=INF;
while(!q.empty())q.pop();
q.push({1,S,0});dis[1][S]=0;
while(!q.empty()){
LL u=q.top().u,hav=q.top().hav,ds=q.top().step;
q.pop();
for(int i=0;i<g[u].size();i++){
LL v=g[u][i].v,cst=g[u][i].cst,tim=g[u][i].tim;
if(hav>=cst&&dis[v][hav-cst]>ds+tim){
dis[v][hav-cst]=ds+tim;
q.push({v,hav-cst,ds+tim});
}
}
if(dis[u][min(mxcst,hav+c[u])]>ds+d[u]){
dis[u][min(mxcst,hav+c[u])]=ds+d[u];
q.push({u,min(mxcst,hav+c[u]),ds+d[u]});
}
}
return;
}
int main(){
n=read(),m=read(),S=read();
for(int i=1;i<=m;i++){
int x=read(),y=read(),v=read(),w=read();
g[x].pb({y,v,w}),g[y].pb({x,v,w});mxcst+=v;
}
S=min(S,mxcst);
for(int i=1;i<=n;i++)c[i]=read(),d[i]=read();
Dijkstra();
for(int i=1;i<=n;i++)ans[i]=INF;
for(int i=1;i<=n;i++)
for(int j=0;j<=mxcst;j++)
ans[i]=min(ans[i],dis[i][j]);
for(int i=2;i<=n;i++)
cout<<ans[i]<<"\n";
return 0;
}
/*
A 和 N 都特别特别小
这一定是突破口,哪怕是之一
但是你需要处理银币的来源啊
就是兑换银币那些怎么处理啊?
显然不能真的存数量因为很大很大很大
但是我们可以存花费的数量
然后再对应算怎么去兑换最优吗?
不行 花费的数量是逐步累加的
怎么这么像……曾经有一次 CSP 的一个加油的题目……啊……
是真的像……咋说呢……嗯……
对了对了 AT 输出样例是不是有分??
*/
- 时间复杂度:\(O(n^2A \log{nA})\)
- 空间复杂度:\(O(n^2A)\)
总结
前两题不说啥;C 往类似 Floyd 的方向上撞了,浪费了点时间;D 做的很慢,主要是每次都在思路没确定的情况下就开写了导致浪费很多时间,以及没发现是逆序对时不应该的,陷入颜色死胡同了;E 最开始写代码时没有想清楚怎么构造、怎么计算代价导致耽误了很多时间,以及没有发现直接枚举 \(26\) 个字母中出现次数的更优方式,提出批评;F 没有做出来很可惜啊,赛时一直在想怎么用更简单的做法把剩余银币数表示出来,却没去想银币数最大的使用上限,鉴定为没把 25 J T4 打一遍导致的,并且最后反应出了正解但是又在某个细节处理上绕了弯最终导致没做出来。
明天会是什么呢……?

浙公网安备 33010602011771号