[正睿集训2021] 模拟赛1
令牌生成
题目描述
设 \(F(q)\) 表示小于等于 \(q\) 的数中可以表示成 \(2^x-2^y\) 形式的正整数的个数,其中 \(x,y\) 都是非负整数。
考虑将所有 \(F(q)=n\) 的 \(q\) 按照二进制表示中的 \(1\) 个数排序,如果有 \(1\) 数量相等的按照原数大小排序。
在排好序的序列中选取第 \(k\) 个数对 \(998244353\) 取模的结果作为令牌,如果序列的长度小于 \(k\),输出 \(-1\) 表示获取令牌失败。给定 \(n,k\) 要求回答多次询问。
\(1\leq n,k\leq 10^{18},1\leq T\leq 100000\)
解法
你看着这道题好像很牛皮的样子,其实就是一个大模拟。
考虑 \(2^x-2^y\) 的数长成什么样子,肯定是前面一堆 \(1\),后面接的全是 \(0\):
那么答案一定是夹在上面两个数之间,这两个数是可以算出来的。
然后考虑在后面的 \(cnt\) 个 \(0\) 里面填 \(1\),如果填入 \(i\) 个 \(1\),那么方案数是 \(C(cnt,i)\),可以证明只用求 \(O(\log k)\) 次组合数就可以得到足够的数,小于 \(1e3\) 的组合数可以预处理,剩下的组合数暴力算不会用多少时间。
然后考虑 \(1\) 填的位置,可以二分 \(1\) 的位置 \(l\),那么方案数是 \(C(l,...)\),这个部分也是 \(O(\log^2 k)\) 的吧。
串
题目描述
给定 \(n\) 个串,有 \(q\) 次询问,每次询问两个串的最长公共子串。
\(1\leq n\leq 50000\),\(1\leq q\leq 100000\),串的总长小于 \(50000\)
解法
首先求两个串的最长公共子串是 \(O(\min(len_x,len_y))\) 的,把小的子串丢进大的里面匹配就可以了。
然后我用到了三元环计数的思想,把长度大于 \(\sqrt n\) 的串称为大串,否则称为小串,分别来算贡献:
- 对于小串,至多 \(q\) 次询问,那么时间复杂度 \(O(q\sqrt n)\)
- 对于大串,它产生贡献的时候当且仅当遇到了大串,至多产生 \(O(\sqrt n)\),那么它的复杂度是 \(O(\sqrt n\cdot m_i)\),又因为 \(\sum m_i\leq n\),时间复杂度 \(O(n\sqrt n)\)
写的时候注意要记忆化时间复杂度才是真的。
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 100005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,ans[M];vector<int> s[M];char st[M];
struct ask
{
int x,id;
ask(int X=0,int I=0) : x(X) , id(I) {}
bool operator < (const ask &R) const
{
return x<R.x;
}
};vector<ask> q[M];
struct node
{
int fa,len,ch[26];
};
struct Sam
{
int cnt,last;node a[2*M];
void init()
{
for(int i=1;i<=cnt;i++)
{
a[i].fa=a[i].len=0;
for(int j=0;j<26;j++)
a[i].ch[j]=0;
}
cnt=last=1;
}
void add(int c)
{
int p=last,np=last=++cnt;
a[np].len=a[p].len+1;
for(;p && !a[p].ch[c];p=a[p].fa) a[p].ch[c]=np;
if(!p) a[np].fa=1;
else
{
int q=a[p].ch[c];
if(a[p].len+1==a[q].len) a[np].fa=q;
else
{
int nq=++cnt;a[nq]=a[q];
a[nq].len=a[p].len+1;
a[q].fa=a[np].fa=nq;
for(;p && a[p].ch[c]==q;p=a[p].fa) a[p].ch[c]=nq;
}
}
}
int work(int t)
{
int ans=0,p=1,len=0;
for(int i=0;i<s[t].size();i++)
{
int c=s[t][i];
while(a[p].fa && !a[p].ch[c])
{
p=a[p].fa;
len=a[p].len;
}
if(a[p].ch[c])
{
p=a[p].ch[c];
len++;
}
ans=max(ans,len);
}
return ans;
}
}A;
int main()
{
n=read();m=read();
for(int i=1;i<=n;i++)
{
scanf("%s",st);
int len=strlen(st);
for(int j=0;j<len;j++)
s[i].push_back(st[j]-'a');
}
for(int i=1;i<=m;i++)
{
int x=read()+1,y=read()+1;
if(s[x].size()>s[y].size()) swap(x,y);
q[y].push_back(ask(x,i));
}
for(int i=1;i<=n;i++)
{
A.init();
for(int j=0;j<s[i].size();j++)
A.add(s[i][j]);
sort(q[i].begin(),q[i].end());
for(int j=0;j<q[i].size();j++)
{
int x=q[i][j].x,id=q[i][j].id;
ans[id]=A.work(x);
while(j<q[i].size()-1 && q[i][j+1].x==x)
{
ans[q[i][j+1].id]=ans[id];
j++;
}
}
}
for(int i=1;i<=m;i++)
printf("%d\n",ans[i]);
}
随机游走
题目描述
有 \(n\) 个点,在点 \(i\) 上走一步的到的点是 \(f_i\),每一轮有 \(p_j\) 的概率走 \(j\in[0,m)\) 步,一开始在每个点的概率都是一样的,求最后到达所有点的概率分别是多少,答案模 \(998244353\)
\(1\leq n\leq60000,1\leq m\leq 100000,k\leq1000\)
部分分:\(f\) 是一个排列
解法
不难想到一个暴力 \(dp\),设 \(dp[i][j]\) 为第 \(j\) 轮在位置 \(i\) 的方案数,\(g(i,j)\) 表示 \(i\) 走 \(j\) 步到的位置,那么有转移:
这个 \(dp\) 难以优化,先讲一下部分分怎么做吧(虽然和正解没有关系),我们把 \(f\) 看成边连起来,因为它是排列所以最后得到了若干个置换环,环内每个点是等价的,那么无论怎么走最后得到的概率都是初始的概率,输出 \(n\) 的逆元就可以了。
正解是生成函数,首先我们要把问题转化为环套树的图,一个点走多次之后会在环里面一直转,设 \(p(x)\) 是走 \(x\) 步概率的生成函数,求出一开始给定 \(p(x)\) 的 \(k\) 次幂即可。
但是这样项数会很多,注意到会有循环,所以对于长度为 \(s\) 这一类 的环,设最大环套数的大小是 \(n\),那我们留足 \(n\) 项就可以了,这一类环套树的 \(p(x)\) 就可以一起处理,把环套树按大小排序就很容易算他们的生成函数了。根据简单的数学知识可知环套树的种类不超过 \(\sqrt n\),此部分时间复杂度 \(O(m\sqrt n+n\log n\log k)\)
对于一个环套树,我们随便选一个环上的点作为根,它连出去的那条边叫非树边。我们先断开这条边建出一棵树,然后分别统计经过非树边和不经过非树边的贡献,图画出来是这个样子,我们顺便给每个点标上深度(正负都没关系):
对于 不经过非树边的贡献 ,一个点仅可能由其子树中的点走到,而子树中点的 \(\tt dfs\) 序又是一段连续的区间,我们把原图按照 \(\tt dfs\) 序分块,我们预处理出整块和 \(p(x)\) 的卷积,零散块可以暴力算,时间复杂度 \(O(n\sqrt{n\log n})\)。
对于 经过非树边的贡献 ,设 \(H(x)=\sum_{i=0}^nh_ix^{n-i}\),其中 \(h_i\) 表示深度为 \(-i\) 的结点有多少个乘上初始概率,那么我们把 \(h_i\) 和 \(p_i\) 求卷积就可以得到最终到环上某点的概率。因为有一些非环上的点也可能混在求出的概率里面,所以减去他们的概率即可。
毒瘤题太难写了,所以没有代码。