[62] (NOIP 集训) NOIP2024加赛 2
lhx: 为啥你不会线性筛,这么重要的东西
lhx: 你不筛积性函数吗
我: 我一般做数学题推不到需要上线性筛的地方
lhx: 哦那你确实用不到
A.新的阶乘
找质数 \(p\) 在式子中的幂,其实就是带权找 \(p\) 在 \([1,n]\) 所有数中的出现次数
发现 \([1,n]\) 中所有数只有形如 \(kp\) 的数能够被找出因子 \(p\)
因此尝试枚举所有 \(p\) 的倍数来更新 \(p\) 的答案
发现这玩意和埃氏筛天作之合,你枚举倍数的时候顺便就把质数筛了,考虑直接上埃氏筛
因为不会怎么求因子出现个数于是用了复杂度很假的暴力做法
理论 \(n\log\log n\log n\),实测 0.3s
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n;
bool notprime[10000001];
vector<int>ans;
inline int powof(int i,int j){
int res=0;
while(j){
if(j%i==0) j/=i,res++;
else break;
}
return res;
}
signed main(){
cin>>n;
notprime[0]=true;
notprime[1]=true;
for(int i=2;i<=n;++i){
if(notprime[i]==false){
ans.push_back(i);
int cnt=n-i+1;
for(int j=2;j*i<=n;++j){
notprime[i*j]=true;
cnt+=(powof(i,j)+1)*(n-j*i+1);
}
ans.push_back(cnt);
}
}
cout<<"f("<<n<<")=";
if(n<=1) cout<<n;
for(int i=0;i<=(int)ans.size()-1;i+=2){
if(i!=0) cout<<'*';
if(ans[i+1]==1){
cout<<ans[i];
}
else{
cout<<ans[i]<<'^'<<ans[i+1];
}
}
}
B.博弈树
写爆搜打表找规律发现:所有节点中,Bob 至多只有一个
爆搜
#include<bits/stdc++.h>
using namespace std;
int n,q;
vector<int>e[1001];
int deep[1001],fa[10][1001],w[10][1001];
void dfs(int now,int last){
deep[now]=deep[last]+1;
fa[0][now]=last;
for(int i:e[now]){
if(i!=last){
w[0][i]=1;
dfs(i,now);
}
}
}
inline void prework(){
for(int i=1;i<=9;++i){
for(int j=1;j<=n;++j){
fa[i][j]=fa[i-1][fa[i-1][j]];
w[i][j]=w[i-1][j]+w[i-1][fa[i-1][j]];
}
}
}
inline int dis(int x,int y){
if(deep[x]<deep[y]) swap(x,y);
int res=0;
for(int i=9;i>=0;--i){
if(deep[fa[i][x]]>=deep[y]){
res+=w[i][x];
x=fa[i][x];
}
}
if(x==y) return res;
for(int i=9;i>=0;--i){
if(fa[i][x]!=fa[i][y]){
res+=w[i][x]+w[i][y];
x=fa[i][x];
y=fa[i][y];
}
}
return res+w[0][x]+w[0][y];
}
//true = win
bool bydfs(int now,int lastdis){
for(int i=1;i<=n;++i){
if(i!=now){
int tmp=dis(i,now);
if(tmp>lastdis){
if(bydfs(i,tmp)==false) return true;
}
}
}
return false;
}
int main(){
cin>>n>>q;
for(int i=1;i<=n-1;++i){
int x,y;cin>>x>>y;
e[x].push_back(y);
e[y].push_back(x);
}
dfs(1,0);
prework();
while(q--){
int t;cin>>t;
cout<<(bydfs(t,0)?"Alice":"Bob")<<'\n';
}
}
而且这个点离树的中心极其近
额外发现当树的重心有两个的时候,没有 Bob
尝试直接特判树的重心假完了
手摸了出错的数据发现,应该是自己树的重心这个思路不对,考虑到 Bob 和 Alice 的博弈过程,这里应该是某种和树上距离有关的 “重心”
于是想出了正确的结论
- 定义重心为使得到所有点的距离的最大值最小的节点
- 如果重心只有一个,Bob 会在此处获胜,否则均为 Alice 获胜
证明:
考虑如果先手在直径的一个端点上,可以通过移动到另一个端点来直接胜出,否则不能移动到直径的端点上,否则后手会移动到另一个端点而直接胜出
考虑删除了原树所有直径的端点的树,如果初始点在这棵树上为直径的某一个端点,那么也一定是先手必胜的,因为先手可以将点移动到直径另一个端点,这样后手就一定会将点移动到原树的直径端点上,并且移动的长度一定小于原树直径,这样先手就可以将点移动到原树的另一个直径端点取得胜利
因此后手胜利,当且仅当先手无论如何都会移动到某层的直径端点上,这样的情况只会在只存在一个点不在某层的直径端点上的图上,并且该点恰好为重心
但是这个东西却是 \(n^2\) 的,过不了 \(10^5\)
但是你没必要去枚举所有的点,因为重心一定在树的直径上,而且一定是直径上靠近中点的位置,你直接去判断靠近直径中点那几个点就行了
被 \(n=1\) 卡了,\(n=1\) 无论如何都是 Bob 赢
#include<bits/stdc++.h>
using namespace std;
int n,q;
vector<int>e[100001];
vector<int>zx;
int size[100001];
int maxsonsize[100001];
int deep[100001],fa[20][100001],w[20][100001];
void dfs(int now,int last){
deep[now]=deep[last]+1;
fa[0][now]=last;
for(int i:e[now]){
if(i!=last){
w[0][i]=1;
dfs(i,now);
}
}
}
inline void prework(){
for(int i=1;i<=19;++i){
for(int j=1;j<=n;++j){
fa[i][j]=fa[i-1][fa[i-1][j]];
w[i][j]=w[i-1][j]+w[i-1][fa[i-1][j]];
}
}
}
inline int dis(int x,int y){
if(deep[x]<deep[y]) swap(x,y);
int res=0;
for(int i=19;i>=0;--i){
if(deep[fa[i][x]]>=deep[y]){
res+=w[i][x];
x=fa[i][x];
}
}
if(x==y) return res;
for(int i=19;i>=0;--i){
if(fa[i][x]!=fa[i][y]){
res+=w[i][x]+w[i][y];
x=fa[i][x];
y=fa[i][y];
}
}
return res+w[0][x]+w[0][y];
}
int minsize=0x7fffffff;
void dfs2(int i){
int maxdis=0;
for(int j=1;j<=n;++j){
maxdis=max(maxdis,dis(i,j));
}
if(maxdis<minsize){
minsize=maxdis;
zx={i};
}
else if(maxdis==minsize){
zx.push_back(i);
}
}
int dis5[100001];
int dis6[100001];
int maxdis5,maxdis5id;
int maxdis6,maxdis6id;
void dfs5(int now,int last){
for(int i:e[now]){
if(i!=last){
dis5[i]=dis5[now]+1;
if(dis5[i]>maxdis5){
maxdis5=dis5[i];
maxdis5id=i;
}
dfs5(i,now);
}
}
}
void dfs6(int now,int last){
for(int i:e[now]){
if(i!=last){
dis6[i]=dis6[now]+1;
if(dis6[i]>maxdis6){
maxdis6=dis6[i];
maxdis6id=i;
}
dfs6(i,now);
}
}
}
vector<int>zj;
bool dfs7(int now,int last,int tar){
if(now==tar){
zj.push_back(now);
return true;
}
for(int i:e[now]){
if(i!=last){
if(dfs7(i,now,tar)){
zj.push_back(now);
return true;
}
}
}
return false;
}
void dfs8(){
int tmp=(int)zj.size()/2;
int tmp2=tmp-1;
if(tmp>=0) for(int i:e[zj[tmp]]) dfs2(i);
if(tmp2>=0) for(int i:e[zj[tmp2]]) dfs2(i);
}
int main(){
freopen("tree.in","r",stdin);
freopen("tree.out","w",stdout);
cin>>n>>q;
if(n==1){
while(q--){
cout<<"Bob\n";
}
return 0;
}
for(int i=1;i<=n-1;++i){
int x,y;
cin>>x>>y;
e[x].push_back(y);
e[y].push_back(x);
}
dfs(1,0);
prework();
dfs5(1,0);
dfs6(maxdis5id,0);
dfs7(maxdis5id,0,maxdis6id);
dfs8();
while(q--){
int t;cin>>t;
if(zx.size()!=1ull) cout<<"Alice\n";
else if(zx[0]!=t) cout<<"Alice\n";
else cout<<"Bob\n";
}
}
C.划分
先说 corner case
- \(n=k\)
显然了,只有唯一的划分方式
- 字符串最前面有超过 \(k\) 位全是 \(0\)
这个时候直接在前面划就行了,无论你怎么拆后面带 \(1\) 的字符串都只会让答案更小
设前面有 \(p\) 个 \(0\),划分成 \(k\) 段,需要插 \(k-1\) 块板,注意最后一个 \(0\) 后面也是能插的,答案即为 \(\sum^p_{i=k-1}C_{p}^{i}\),预处理阶乘直接算即可
注意这个 case 有一个全 \(0\) 的特殊情况,在第三个 hack
- 通解
上面两个 corner case 都要判掉再做通解
最优解一定形如选出 \(k-1\) 个单个的区间,剩下一个连续的大区间单独分一段(区间越长高位贡献越大)
这样剩余的情况就只有 \(k\) 种了,可以暴力判断哪一种情况更优
因为 \(1\) 的个数一样,实际上谁的高位 \(1\) 个数更多,谁的答案就更大
发现有人不理解这句话:实际上,因为你 \(1\) 的个数是固定的,而每个 \(1\) 都会给答案带来一个 \(2^k\) 的贡献(\(k\) 是 \(1\) 所在的位置),因此你肯定是要最大化每个 \(k\) 的值,也就是高位 \(1\) 个数尽可能多/高,而我们说最低位和单独的 \(1\) 没用,是因为无论怎么分,每个 \(1\) 总是会有至少 \(2^0\) 的贡献,相当于最低位和单独的 \(1\) 没做出任何额外贡献,在这一点上二者是等价的
因此只比较那个长区间,直接暴力比较是 \(O(len)\) 的,为了规避暴力比较,可以用二分+哈希求出需要比较的两个区间的最长公共前缀,然后直接比较后一位,可以做到 \(O(\log len)\)
这里注意最长公共前缀等于原串的情况,有可能会越界
还是那句话,谁的高位 \(1\) 个数更多,谁的答案就更大,因此我们判方案数的时候也是同理,因为最低位的 \(1\) 和单独的 \(1\) 贡献是一样的,因此两个串答案相同,当且仅当最长的区间去除最低位后相等,依旧用哈希判断
注意卡模数的问题
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int p=998244353;
const int P1=1e9+3,P2=1e7+0721;
int n,k;
string x;
int power(int a,int t){
int base=a,ans=1;
while(t){
if(t&1){
ans=ans*base%p;
}
base=base*base%p;
t>>=1;
}
return ans;
}
int fact[2000001];
int C(int n,int m){
return fact[n]*power(fact[m]*fact[n-m]%p,p-2)%p;
}
int cal(int l,int r){
int res=0;
for(int i=l;i<=r;++i){
res=(res*2+x[i]-'0')%p;
}
return res;
}
const unsigned long long num=233,num2=2333;
unsigned long long h[2000001],basenum[2000001];
unsigned long long geth(int l,int r){
if(l>r) return 0;
return (h[r]-h[l-1]*basenum[r-l+1]%P1+P1)%P1;
}
unsigned long long h2[2000001],basenum2[2000001];
unsigned long long geth2(int l,int r){
if(l>r) return 0;
return (h2[r]-h2[l-1]*basenum2[r-l+1]%P2+P2)%P2;
}
bool great(int now,int ori){
int l=0,r=n-k+1,ans=0;
while(l<=r){
int mid=(l+r)/2;
if(geth(now,now+mid-1)==geth(ori,ori+mid-1) and geth2(now,now+mid-1)==geth2(ori,ori+mid-1)){
l=mid+1;
ans=mid;
}
else r=mid-1;
}
if(ans==n-k+1) return false;
return x[now+ans]>x[ori+ans];
}
inline int cal2(int l,int r){
int res=0;
for(int i=1;i<l;++i){
res=(res+x[i]-'0')%p;
}
int res2=0;
for(int i=l;i<=r;++i){
res2=(res2*2+x[i]-'0')%p;
}
res=(res+res2)%p;
for(int i=r+1;i<=n;++i){
res=(res+x[i]-'0')%p;
}
return res;
}
signed main(){
cin>>n>>k>>x;x=" "+x;
if(n==k){
int res=0;
for(int i=1;i<=n;++i){
res=(res+x[i]-'0')%p;
}
cout<<res<<" "<<1;
return 0;
}
fact[0]=1;
basenum[0]=1;
basenum2[0]=1;
for(int i=1;i<=n;++i){
fact[i]=fact[i-1]*i%p;
h[i]=(h[i-1]*num+x[i])%P1;
basenum[i]=basenum[i-1]*num%P1;
h2[i]=(h2[i-1]*num2+x[i])%P2;
basenum2[i]=basenum2[i-1]*num2%P2;
}
int first1=n;
for(int i=1;i<=n;++i){
if(x[i]=='1'){
first1=i;
break;
}
}
if(first1>=k){
cout<<cal(1,n)<<' ';
int res=0;
for(int i=k-1;i<=first1-1;++i){
res=(res+C(first1-1,i))%p;
}
cout<<res;
return 0;
}
int maxid=0;
for(int i=1;i<=k;++i){
if(maxid==0) maxid=i;
else{
if(great(i,maxid)){
maxid=i;
}
}
}
cout<<cal2(maxid,maxid+n-k)<<" ";
int res=0;
for(int i=1;i<=k;++i){
if(geth(i,i+n-k-1)==geth(maxid,maxid+n-k-1) and geth2(i,i+n-k-1)==geth2(maxid,maxid+n-k-1)) res++;
}
cout<<res;
}