20260810 - 暑假热身 总结
前言
总结好吃吃掉总结。哎完蛋了,没总结给你看了,我总结被我吃了。
mmt:前五题是送的。
:我宣称前七题是送的。
【数据删除】:?你看这家伙在说什么呢。
cn:……I 题讲完了,大家有问题吗?
【数据删除】:没事不需要听懂,等 rmx 写完总结就可以抄 Ta 代码了。
:?那我总结不放这题代码了不就行了??
hhh 不知道 hhh。
我居然没做出来后两题,菜完了,废物完了,唉。
A - All Lengths Subtraction
水题,\(n\) 很小可以直接暴力搞,第 \(i\) 次需要把当前为 \(n-i+1\) 的全部 \(-1\),每次判断一下是否是连续的就行了。
戳这里看代码
#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)\)。
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;
int n,a[N],d[N];
bool vis[N];
vector<int> ans;
queue<int> 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;
}
int main(){
n=read();
for(int i=1;i<=n;i++)
a[i]=read(),d[a[i]]++;
for(int i=1;i<=n;i++)
if(d[i]==0)q.push(i);
while(!q.empty()){
int u=q.front();q.pop();
vis[u]=1;--d[a[u]];
if(d[a[u]]==0)q.push(a[u]);
}
for(int i=1;i<=n;i++)if(!vis[i]){
int x=a[i];ans.pb(i);
while(x!=i)ans.pb(x),x=a[x];
break;
}
cout<<ans.size()<<"\n";
for(int i=0;i<ans.size();i++)
cout<<ans[i]<<" ";cout<<"\n";
return 0;
}
时间复杂度:\(O(n)\)(含有若干常数)。
C - Adding Powers
进制拆分具有唯一性,那我们把这些数的进制全部拆出来,塞桶里看看有没有重复的就行了。
戳这里看代码
#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;
LL T,n,k,a[105],mx,up;
bool vis[105],flag;
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(){
T=read();
while(T--){
n=read(),k=read(),mx=0;
for(int i=1;i<=n;i++)
a[i]=read(),mx=max(mx,a[i]);
up=-1;LL tmp=1;
while(tmp<=mx)tmp*=k,up++;
for(int i=0;i<=100;i++)vis[i]=0;
flag=1;
for(int i=1;i<=n;i++){
LL now=tmp/k;
for(int x=up;x>=0;x--){
if(a[i]>=now){
a[i]-=now;
if(vis[x])flag=0;
vis[x]=1;
}
now/=k;
}
if(a[i])flag=0;
}
if(flag)cout<<"YES\n";
else cout<<"NO\n";
}
return 0;
}
时间复杂度:\(O(T n \log n)\)。
D - Find Permutation
为啥这么喜欢出拓扑排序?
建图,从小的连到大的,然后还是拓扑排序(和 B 题一样),只不过这里要判断一下在取出当前队头的情况下,队列一定要是空的,也就是队列在任何时刻都必须至多含有 \(1\) 个元素。再维护一个赋值变量 \(cnt\) 从小到大累加,如果合法最后也一定 \(cnt = 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;
int n,m,d[N],a[N],cnt;
vector<int> g[N];
queue<int> q;
bool flag=0;
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(),m=read();
for(int i=1;i<=m;i++){
int x=read(),y=read();
g[x].pb(y),d[y]++;
}
for(int i=1;i<=n;i++)
if(d[i]==0)q.push(i);
while(!q.empty()){
int u=q.front();q.pop();
if(!q.empty())flag=1;
cnt++,a[u]=cnt;
for(int i=0;i<g[u].size();i++){
int v=g[u][i];
d[v]--;if(!d[v])q.push(v);
}
}
if(cnt!=n){
cout<<"No\n";
return 0;
}
if(flag){
cout<<"No\n";
return 0;
}
cout<<"Yes\n";
for(int i=1;i<=n;i++)
cout<<a[i]<<" ";cout<<"\n";
return 0;
}
时间复杂度:\(O(n+m)\)。
E - Flower Boy
感觉从这题开始就上了点难度了,我还是想了一会儿的。
首先你可以想到一个贪心,就是你在采花的过程中只要这朵花满足条件就一定要采,可以证明这样不会变的更劣。
然后就是,大概就是你发现这个魔法变出来的花会把原序列分成两个部分,一个是 \([1,i]\) 一个是 \([i+1,n]\)。好吧这是废话但是有用,我们考虑前后缀分别最多能采多少朵花(后缀要倒着采),记为 \(x\) 和 \(y\)。所以分三种情况,一种是 \(x+y \ge m\),这说明不需要用魔法就能完成任务;一种是 \(x+y+1=m\),这时候需要在中间插入一朵价值为 \(b_{x+1}\) 的花;还有是 \(x+y+1<m\),这种没救,对答案无贡献。
然后就没然后了,代码注释里有做法也可以看看(赛时写的)。
戳这里看代码
#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;
const int INF = 2e9;
int T,n,m,a[N],b[N],dp[N],f[N],Ans;
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(),m=read();
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1;i<=m;i++)b[i]=read();
dp[0]=0,f[n+1]=0;
for(int i=1;i<=n;i++)
dp[i]=dp[i-1]+(dp[i-1]<m?b[dp[i-1]+1]<=a[i]:0);
for(int i=n;i>=1;i--)
f[i]=f[i+1]+(f[i+1]<m?b[m-f[i+1]]<=a[i]:0);
Ans=INF;
for(int i=0;i<=n;i++){
int x=dp[i],y=f[i+1];
if(x+y>=m)Ans=0;
else if(x+y==m-1)Ans=min(Ans,b[x+1]);
}
if(Ans==INF)Ans=-1;
cout<<Ans<<"\n";
}
return 0;
}
/*
顺着倒着维护两个 dp 数组
dp[i] 表示前 i 个数最多能选多少花
f[i] 则是后 i 个数(倒着选)
然后枚举 i,i+1 作为分界点在这中间用魔法
令前面的花数为 x,后面的为 y
那么当 x+y=m 时输出 0
当 x+y=m-1 时答案可能为 b[x+1]
当 x+y<m-1 时这个分界点不行
*/
时间复杂度:\(O(Tn)\)。
F - Greedy Gift Takers P
你们知道吗,讲题讲的模糊程度能做到和讲题者一样做法做这道题的人都完全听不懂的,那这个讲题者也很牛逼了嗯。
首先这个东西,我们发现它有一个性质,就是永远没法收到礼物的奶牛一定是一段后缀。证明显然懒得说了。
也就是说有单调性对吧,考虑二分。check 怎么写呢?
首先我们发现,对于当前的 \(mid\) 这个位置上的奶牛,它当前是倒数第 \(n-mid+1\) 个,记作 \(rk\) 吧。那么对于它前面的一个 \(c_i\) 奶牛,如果前面这个奶牛的 \(c_i < rk\)(\(rk\) 是当前 \(rk\)),那么这个奶牛就会死到最开始的 \(mid\) 这个奶牛的后面去,就会被堵死在后面,也就不需要考虑它了,因此 \(rk \gets rk + 1\);否则这个奶牛就还会待在前面,等待下一次轮到它往后走。
于是就可以非常 naive 的写出一个 check,然而显然是错的,因为待在前面的奶牛不一定会一直待在前面,会有机会往后走,但可能是好几轮之后。
这种情况咋搞呢?精华就是——排序!我们发现 \(c_i\) 越小越容易满足条件,所以从小到大排序后再逐一判断并维护当前 \(rk\),最后看是否 \(rk = 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,c[N],l,r,ans;
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 check(int pos){
int now=n-pos+1;
vector<pii> tmp;tmp.clear();
for(int i=1;i<pos;i++)
tmp.pb(make_pair(c[i],i));
sort(tmp.begin(),tmp.end());
for(int i=0;i<tmp.size();i++){
int x=tmp[i].fr;
if(x<now)now++;
}
return (now==n);
}
int main(){
n=read();
for(int i=1;i<=n;i++)c[i]=read();
l=1,r=n,ans=0;
while(l<=r){
int mid=(l+r)>>1;
if(check(mid))ans=mid,l=mid+1;
else r=mid-1;
}cout<<n-ans<<"\n";
return 0;
}
时间复杂度:\(O(n \log n)\)。
G - 回文匹配
又是子串又是回文的,先跑个 Manacher 再跑个 KMP 肯定没啥问题。
然后就卡在最后一步了,咋统计数量呢?
首先 \(s_2\) 长度固定所以可以在统计数量的时候只维护左端点或者右端点,但是这样还是会出现 \(O(n^2)\) 的情况(好比整个序列都是 a 导致回文串数量很多之类),考虑优化。
\(O(n^2)\) 的做法会用到差分,差分每次都是连续一段的加或减,这就可以对差分数组做差分,然后两次前缀和还原即可。
不想讲了,看代码吧。
戳这里看代码
#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 = 6e6+6;
int n,m,k,p[N],nxt[N];
UInt c[N],Ans;
string s1,s2,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;
}
//void upd(int x,ULL k){while(x<=n)c[x]+=k,x+=x&-x;return;}
//ULL ask(int x){ULL sum=0;while(x)sum+=c[x],x-=x&-x;return sum;}
int main(){
n=read(),m=read();
cin>>s1>>s2;
s=" "+s2+"#"+s1;k=s.size()-1;
s1=" "+s1,s2=" "+s2;
for(int i=1,l=0,r=-1;i<=n;i++){
int len=(i>r?1:min(p[l+r-i],r-i+1));
while(len<i&&i+len<=n&&s1[i-len]==s1[i+len])len++;
p[i]=len;
if(i+len-1>r)l=i-len+1,r=i+len-1;
}
for(int i=2;i<=k;i++){
int j=i-1;
while(j){
if(s[nxt[j]+1]==s[i])
{nxt[i]=nxt[j]+1;break;}
j=nxt[j];
}
}
for(int i=1;i<=n;i++){
if(2*p[i]-1<m)continue;
//int l=0,r=0;
int x=i-p[i]+1,y=i+p[i]-1;
//cout<<"x="<<x<<" , y="<<y<<"\n";
int k=(y-x+1-m)/2;
//cout<<x<<"->"<<x+k<<" "<<y-k-m+2<<"->"<<y-m+2<<" ?\n";
c[x]++,c[x+k+1]--;
c[y-k-m+2]--,c[y-m+3]++;
// while(y-x+1>=m){
// l=x,r=y-m+2;
// x++,y--;
// }
// cout<<l<<" "<<r<<"awa\n";
}
for(int i=1;i<=n;i++)c[i]+=c[i-1];
//for(int i=1;i<=n;i++)cout<<c[i]<<" ";cout<<"\n";
for(int i=1;i<=n;i++)c[i]+=c[i-1];
for(int i=m+2;i<=k;i++){
int ii=i-m-1;
if(nxt[i]==m){
//cout<<ii-m+1<<"!!!\n";
Ans+=c[ii-m+1];
}
}
cout<<Ans<<"\n";
return 0;
}
时间复杂度:\(O(n+m)\)(有一定常数)。
H - Level Up
很简单的一个题其实,但是 \(40\) 分钟没有想出来属于我菜。
首先单调性是显然的,\(k\) 越小,这个人的等级就提升的越快,那怪物逃跑的可能性就越高,所以具有单调性考虑二分。
考虑对每个怪物找出一个 \(b_i\),表示当 \(k < b_i\) 那么这个怪物就会逃跑,否则 \(k \ge b_i\) 这个怪物就会留下来战斗。
二分是没问题的,但是怎么 check 呢?发现第 \(i\) 个怪物的状态依赖于前 \(i-1\) 个怪物,所以考虑顺序枚举这个 \(i\) 按照这样的顺序依赖前面已经求出来的 \(b_1 \sim b_{i-1}\) 来算 \(b_i\)。
由于我们二分一个 \(mid\) 的时候需要求出前面 \(i-1\) 轮有多少个怪物打架了,而 \(b_1 \sim b_{i-1}\) 又都已知,所以我们只需要算 \(\le mid\) 的 \(b_j(j<i)\) 有多少个,记为 \(cnt\) 个,那么在准备打第 \(i\) 个怪物时,人的血量就是 \(\lfloor \frac{cnt}{now} \rfloor + 1\)。(同时从这里也可以看出单调性)
那么 check 函数就写完了,但是是 \(O(n)\) 的,综合在一起 \(O(n^2 \log n)\) 咋办?哎这很简单,因为时间复杂度瓶颈在于算 \(\le mid\) 的 \(b_j\) 个数,而这可以用权值树状数组轻轻松松秒掉,所以时间复杂度来到了优秀的 \(O(n \log^2n)\)。
然后查询的时候直接判就行了,\(O(1)\) 的。
戳这里看代码
#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;
int n,Q,a[N],b[N];
int t[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)t[x]+=k,x+=x&-x;return;}
int ask(int x){int sum=0;while(x)sum+=t[x],x-=x&-x;return sum;}
int check(int now){
int cnt=ask(now);
return cnt/now+1;
}
int main(){
n=read(),Q=read();
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1;i<=n;i++){
//<b[i]会逃跑 >=b[i]会战斗
int l=1,r=n,ans=1;
while(l<=r){
int mid=(l+r)>>1;
//check(mid)是当前人的实力
//如果check(mid)<=a[i]说明人实力太差要打架
if(check(mid)<=a[i])ans=mid,r=mid-1;
else l=mid+1;
}b[i]=ans,upd(b[i],1);
}
while(Q--){
int id=read(),x=read();
if(x<b[id])cout<<"NO\n";
else cout<<"YES\n";
}
return 0;
}
时间复杂度:\(O(n \log^2n + Q)\)。
I - Distance to Different
这个题目稍微需要一点思考。
首先需要发现一个性质,那就是说我们可以把序列将同一类数字划分为一段,那么这一段的 \(b\) 值就是形如 \(1,2,3,\dots,k-1,k,k-1,k-2,\dots,2,1\) 或者 \(1,2,3,\dots,k-1,k,k,k-1,\dots,2,1\) 的(如果在两边,那就是形如 \(1,2,\dots,k\) 或 \(k,k-1,\dots,1\)),啊那这个东西只和段的长度有关呀,和具体是什么值没有任何关系。不过,唯一要注意的是段数必须 \(\ge k\),因为题目要求每种数至少要出现一次。
那么就可以 DP 了,令 \(dp_{i,k}\) 表示当前考虑了前 \(i\) 个位置且段数为 \(k\) 的情况下的总方案数,发现转移也是非常轻松,直接一个 \(dp_{i,k} = \sum_{j=1}^{i-1} dp_{j,k-1}\) 就行了,对吧?不对不对,因为长度为 \(2\) 的段在中间是 \(1,1\),而两个连续的长度为 \(1\) 的段那也是 \(1,1\),会重复计数,所以长度为 \(2\) 的段只能出现在两边。
这样我们就收获了一个 \(O(n^3)\) 的时间和 \(O(n^2)\) 的空间,爆炸!首先可以优化掉 \(k\) 那一维,因为 \(> k\) 的都可以看做 \(k+1\)。这样就是 \(O(n^2k)\) 的时间和 \(O(nk)\) 的空间,虽然空间没问题了,但时间还是过不去啊??没关系,我们发现每次我们求的其实是一个前缀和的转移,那么可以直接做一个前缀和维护,这样就可以再降一维时间了。
最后就可以过了,额外需要注意一下取模相关的事项。
戳这里看代码
#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;
const LL MOD = 998244353;
LL n,k,dp[N][15],sum[N][15];
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(),k=read();
dp[0][0]=1,sum[0][0]=1;
for(int i=1;i<=n;i++){
sum[i][0]=1;
for(int j=1;j<=k;j++){
dp[i][j]=sum[i-1][j-1];
if(i>2&&i<n)dp[i][j]=(dp[i][j]-dp[i-2][j-1]+MOD)%MOD;
sum[i][j]=(sum[i-1][j]+dp[i][j])%MOD;
}
dp[i][k+1]=(sum[i-1][k]+sum[i-1][k+1])%MOD;
if(i>2&&i<n)dp[i][k+1]=(dp[i][k+1]-dp[i-2][k]+MOD-dp[i-2][k+1]+MOD)%MOD;
sum[i][k+1]=(sum[i-1][k+1]+dp[i][k+1])%MOD;
}
cout<<(dp[n][k]+dp[n][k+1])%MOD;
return 0;
}
时间复杂度:\(O(nk)\)。
总结
ABCDE 没啥说的;F 稍微做的有点慢,在 check 函数的实现那里卡了一小会;G 题没有在最快速的时间内先把 \(O(n^2)\) 暴力打掉,因为写完暴力就可以发现是差分套差分,可以二阶差分解决(别管了我不知道这东西叫什么名字);H 题挺可惜的,我发现了单调性,但没有想到可以这样联系之前的答案做 check,属于是一个新的 trick(吧?);I 题倒是没怎么细想,也没有发现这个段的性质,做不出来也还算正常。
明天不会是石山吧(害怕 ing

浙公网安备 33010602011771号