OI 笑传 #5

这场模拟赛是打过的感觉最好的一场模拟赛,虽然也挂了分但是从每个题里面都能或多或少的学到点什么。

写这个的时候已经忘了赛时干过什么了,来看题吧。

T1

场切了,但是貌似挂了不少人的分?

考虑 \(\gcd(a,b)\)\(\operatorname{lcm}(a,b)\) 的实质,我们把 \(a,b\) 质因数分解为 \(a=p_1^{i_1}p_2^{i_2}p_3^{i_3}\cdots\)\(b=p_1^{j_1}p_2^{j_2}p_3^{j_3}\cdots\),于是有:

\[\gcd(a,b)=p_1^{\min(i_1,j_1)}p_2^{\min(i_2,j_2)}p_3^{\min(i_3,j_3)}\cdots \]

\[\operatorname{lcm}(a,b)=p_1^{\max(i_1,j_1)}p_2^{\max(i_2,j_2)}p_3^{\max(i_3,j_3)}\cdots \]

回到这个题,我们先把 \(x\) 质因数分解:\(x=p_1^{k_1}p_2^{k_2}p_3^{k_3}\cdots\)

首先如果 \(p=1\),那么当然只有自己是合法的,答案是 \(1\)

如果 \(p>1\),我们可以改变通过改变 \(x\) 的某个 \(p_i\) 上的 \(k_i\)\(k_i\times p\)\(\frac{k_i}{p}\) 来构造 \(y\),改变为后者的条件是 \(k_i\) 可被 \(p\) 整除。

这里要理解一下,通过上面的式子手摸一下就明白了。

于是质因数分解完对于每一位乘法原理计算即可。注意特判 \(p=1\)

code

Show me the code
#define psb push_back
#define mkp make_pair
#define ls p<<1
#define rs (p<<1)+1
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
bool ispr[1000005];
int pl[1000005];
bitset<1000000> vis; 
int num=0;
bool j(ll s){
	for(int i=2;i*i<=s;i++){
		if(s%i==0)return 0;
	}
	return 1;
}
void es(int n){
	memset(ispr,1 ,sizeof ispr);
	ispr[1]=0;
	for(int i=2;i<=n;i++){
		if(ispr[i])pl[++num]=i; 
		for(int j=1;j<=num&&i*pl[j]<=n;j++){
			ispr[i*pl[j]]=0;
			if(i%pl[j]==0)break;
		}
	}
}
map<ll,ll> mp;
void diver(ll x){
  int c=1;
  while(x){
    if(j(x)){
      mp[x]++;
      break;
    }
    if(x%pl[c]==0){
      x/=pl[c];
      mp[pl[c]]++;
    }
    else c++;
  }
  return ;
}
int main(){
  
  // freopen("count.in","r",stdin);
  // freopen("count.out","w",stdout);

  ll x,p;cin>>x>>p;
  if(p==1){
    cout<<1;return 0;
  }
  es(1e5);
  diver(x);
  ll ans=1;
  for(auto v:mp){
    if(v.second%p==0)ans*=2;
  }
  cout<<ans;
  
  return 0;
}

T2

有一种做法是优先队列存下来两两相邻的数加和的最大值,考虑每次删去产生了最大值的那一对元素中较大的那一个,因为只有这样才可能让答案更优,即使删去后序列的权值变大。

赛时我还注意到了一个性质:如果我们能够构造出一个长度 \(\ge m\) 的,权值小于给定值的串,我们一定可以通过每次删去序列中最大值的方式,来将其变成一个长度正好为 \(m\) 的,权值小于给定值的串。

证明反证即可。

然后问题变成了给定一个权值 \(k\),我们能构造出的最长的符合要求的串的长度是多少。

这里是最恶心的地方,因为题目中要求是一个环,不能直接贪心了。如果直接 DP 会有 \(O(n^3)\) 的时间复杂度。

有个很重要的观察是:在最长的符合要求的串中,序列的最小值是一定在串中的。

原因是因为是全局最小值,换成序列里的其它值一定不优。

于是我们完全可以钦定最长串的开头是最小值,也就是在最小值处断环成链,接下来想想怎么贪心。

还有一个好玩的小技巧是我们根据 \(\left \lfloor \frac{k}{2} \right \rfloor\) 的大小把序列的权值分类,接下来所有的权值 \(\le \left \lfloor \frac{k}{2} \right \rfloor\) 的元素都可以之间选进合法序列里去了,原因显然。

接下来考虑已经放进去的元素之间那些段里面权值 \(\ge \left \lfloor \frac{k}{2} \right \rfloor\),我们尝试把里面最小的元素放进去,如果这个元素和两边的加和 \(\le k\),那么可以放进去,反正不能,如此贪心即可。

但是 code 还没调过,可能不会再调了罢。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define ls p<<1
#define rs (p<<1)+1
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=2e5+5;
int a[N];
int c[N];
int n,m;
int le[N],lepp[N],re[N],repp[N];
bool check(ll mid){
  int lp=0,rp=0;
  memset(le,0,sizeof le);memset(lepp,0,sizeof lepp);
  memset(re,0,sizeof re);memset(repp,0,sizeof repp);
  for(int i=1;i<=n+1;i++){
    if(c[i] <= mid/2)le[++lp]=c[i],lepp[lp]=i;
    else re[++rp]=c[i],repp[rp]=i;
  }
  repp[rp+1]=0x3f3f3f3f;
  int cnt=0;
  int p=1,q=1;
  lp--;
  bool vi=0;
  while(p<=lp&&q<=rp){
  	if(lepp[p]<repp[q]){
  		p++;cnt++;vi=0;
		}
		if(p==lp+1){
			if(le[p-1]+re[q]<=mid){cnt++;break;}
			else break;
		}
		if(lepp[p]>repp[q]){
			if(!vi)q++,cnt++,vi=1;
			else q++;
		}
	}
	while(p<=lp)cnt++,p++;
	if(cnt>=m)return 1;
	else return 0;
}
int main(){
  
  cin>>n>>m;
  rep(i,1,n)cin>>a[i];
  pair<int,int> mi;
  mi.first=0x3f3f3f3f;
  rep(i,1,n){
    if(a[i]<mi.first){
      mi.first=a[i];
      mi.second=i;
    }
  }
  int cnt=1;
  for(int i=mi.second;i<=n+mi.second;i++){
    if(i>n) c[cnt++]=a[i%n];
    else c[cnt++]=a[i];
  }
  ll l=0,r=1e9+7;
  while(l<r){
    ll mid=l+r>>1;
    if(check(mid))r=mid;
    else l=mid+1;
  }
  cout<<l;
  
  return 0;
}

T3

一眼可以有一个 \(O(n^2)\) 的 DP,就是设 \(dp_{i,j}\) 表示前 \(i\) 个位置分段的 \(\mathrm{mex}\) 都为 \(j\) 的分段方案数。对于每个 \(i\),向后边算 \(\mathrm{mex}\) 边转移即可。

接下来是观察,由于我们要把整个区间不能删除的分成一些连续段,各段的 \(\mathrm{mex}\) 都相等,于是各段的 \(\mathrm{mex}\) 就是原来序列的 \(\mathrm{mex}\)

这样,我们就可以省去 \(dp\) 数组的一维,接下来考虑转移。

你会发现由于 \(\mathrm{mex}\) 的单调性,所有满足区间 \([j,i]\)\(\mathrm{mex}\) 等于整个序列的 \(\mathrm{mex}\)\(j\) 一定会构成一段前缀。现在要做的就是对于每个 \(i\),快速找到这个前缀开始的位置 \(j\)。之后前缀和一下优化转移即可。

找到这个位置的方法我们从 \(\mathrm{mex}\) 的定义考虑,记录 \(1\sim \mathrm{mex}-1\) 的所有数在 \(i\) 及其之前出现的最大位置,这样所有最大位置的最小值就是 \(j\) 的位置。至于所有 \(\ge \mathrm{mex}\) 的数,显然不需要管的。

于是就做完了,代码比较好写。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define ls p<<1
#define rs (p<<1)+1
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=3e5+5;
const int mod=998244353;
int n;
struct seg{
  int l;int r;
  int m;
}t[N<<2];
int a[N];
int dp[N];
bitset<N> b;
int p=0;
int getmex(){
  while(b[p])p++;
  return p;
}
void bu(int p,int l,int r){
  t[p].l=l;t[p].r=r;
  if(l==r){
    t[p].m=0;
    return ;
  }
  int mid=l+r>>1;
  bu(ls,l,mid);bu(rs,mid+1,r);
  t[p].m=min(t[ls].m,t[rs].m);
  return ;
}
void asi(int p,int pos,int k){
  if(t[p].l==pos&&t[p].r==pos){
    t[p].m=k;return;
  }
  int mid=t[p].l+t[p].r>>1;
  if(pos<=mid)asi(ls,pos,k);
  if(mid<pos) asi(rs,pos,k);
  t[p].m=min(t[ls].m,t[rs].m);
  return ;
}
int qry(){return t[1].m;}
int bi[N];
int reg[N];
int k2[N];
ll ksm(ll v,ll k){
  ll ans=1,f=v;
  while(k){
    if(k&1){
      ans=ans*f%mod;
    }
    f=f*f%mod;
    k>>=1;
  }
  return ans;
}
int main(){
//  
//  ios::sync_with_stdio(0);
//  cin.tie(0);

  // freopen("divide.in","r",stdin);
  // freopen("divide.out","w",stdout);
  
  cin>>n;
  bool le=1;int tmax=0;
  for(int i=1;i<=n;i++){
    cin>>a[i];bi[i]=a[i];
    if(a[i]>25)le=0;
    b[a[i]]=1;
  }
  int mex=getmex();
  if(mex==0){
    cout<<ksm(2,n-1);
    return 0;
  }
  sort(bi+1,bi+1+n);
  int len=unique(bi+1,bi+1+n)-bi-1;
  for(int i=1;i<=len;i++){
  	reg[bi[i]]=i;
	}
	len=reg[mex-1];
  bu(1,1,len);
  b.reset();p=0;
  for(int i=1;i<=n;i++){
    b[a[i]]=1;
    if(getmex()==mex){
    	dp[i]=1;
		}
		// cout<<"stdmex: "<<getmex()<<'\n';
  }
  for(int i=1;i<=n;i++){
	  if(reg[a[i]]<=len)asi(1,reg[a[i]],i);
    int tr=qry();
    // cout<<tr<<'\n';
    if(tr!=0){
    	k2[i]=(dp[i]+dp[tr-1])%mod;
      dp[i]=(dp[i-1]+k2[i])%mod;
    }
  }
  cout<<k2[n]<<'\n';
  
  return 0;
}

T4

看到这题就感觉很颠啊,只有最后一次赋值及其之后的加操作是有用的,而且加操作的顺序不重要,我们还可以任意的选择是否要进行这些加操作,于是就变成了一个背包问题。

具体来说,我们取一个 bitset,初始时把所有的赋值操作对应的数作下标的位置设为 \(1\),然后枚举所有加操作,遍历所有位置 \(i\),若 \(i\) 位置为 \(i\),则把 \((i+v) \bmod p\) 的位置也设置成 \(1\)

于是你就有了 50pts。

接下来考虑这东西还能怎么优化,观察到这种 dp 的转移是永久性的,意义是如果 \(i\) 位置被设为了 \(1\),之后 \(i\) 位置将不会改变,也就是说,我们对这个 bitset 做的有意义的操作至多有 \(p\) 次。

能不能优化掉一些无用的操作呢,这里我们首先把这个取模操作简化掉,具体来说就是断环成链,把序列长度变成 \(2p\)

接下来,考虑将要根据区间 \([l,r]\) 内值的情况,对区间 \([l+v,r+v]\) 进行修改。这里有一点是:如果两个区间完全一样,我们是没有必要去修改目标区间的。

这种势能类的东西之前我们在线段树上见过不少。判断区间是否相等用 hash 即可。这里可用线段树或树状数组维护区间 hash。

如果不同,我们去递归修改。但是这里还有一点,如果我们找到一个位置 \(i\)\(i\) 位置为 \(0\),但是 \(i+v\) 位置是 \(1\),这时候我们不会去更新 \(i\),也就是我们利用了势能,但是却没有消除势能。

但其实这种情况是不会出现的,原因是因为在取模意义下加操作会形成环,环上所有点会同时变成 \(1\)

于是算法的时间复杂度就正确了。

code

Show me the code

posted @ 2025-08-04 17:46  hm2ns  阅读(14)  评论(0)    收藏  举报