ICPC2023杭州站题解(B D E F G H J M)
本场金牌数量较其他场多(45枚),但金牌线题数不多。
五题为分水岭,五道简单题过后所有题均为金牌题,其中有四道可做,即A B E F,做出任意一道即可拿金牌。
这里提供除 A 题以外的所有可做题的题解。
ICPC2023杭州站:
M:
加入比当前选择的所有数大的数一定会让平均值上升,因此答案数列中,V图中的某一侧一定会全部被选择。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <complex>
#include <queue>
#include <map>
using namespace std;
const ll N=801010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
typedef long double db;
inline ll read() {
ll sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
int T;
int n;
ll a[N],sum[N];
int main() {
T = read();
while(T--) {
n = read();
int mi = inf, id = 0;
for(int i=1;i<=n;i++) {
a[i] = read();
sum[i] = sum[i-1] + a[i];
if(a[i]<mi) mi = a[i], id = i;
}
db ans = 0.0;
for(int i=1;i<=id-1;i++) ans = max(ans,(db)(sum[n]-sum[i-1])/(db)(n-i+1));
for(int i=id+1;i<=n;i++) ans = max(ans,(db)(sum[i])/(db)(i));
printf("%.15Lf\n",ans);
}
return 0;
}
J:
交互题,探索一个图是菊花图还是链。
手玩起来还是挺容易的,不讲了。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <complex>
#include <queue>
#include <map>
using namespace std;
const ll N=801010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
typedef long double db;
inline ll read() {
ll sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
int T;
int n;
ll a[N],sum[N];
inline void outing(int cl) {
if(cl) cout<<"! "<<1<<endl;
else cout<<"! "<<2<<endl;
fflush(stdout);
}
inline void solve(int id) {
int wo = id+2, ni = id+3, id2 = id+1;
if(wo>n) wo -= n;
if(ni>n) ni -= n;
if(id2>n) id2 -= n;
cout<<"? "<<id<<" "<<wo<<"\n";
fflush(stdout);
if(read()) {
cout<<"? "<<id<<" "<<ni<<"\n";
fflush(stdout);
if(read()) outing(0);
else outing(1);
}
else {
int ci = 0;
cout<<"? "<<id2<<" "<<ni<<"\n";
fflush(stdout);
ci += read();
cout<<"? "<<id2<<" "<<wo<<"\n";
fflush(stdout);
ci += read();
if(ci==2) outing(0);
else outing(1);
}
}
int main() {
T = read();
while(T--) {
n = read();
int id = 0, num = 0;
for(int i=1;i<n;i+=2) {
cout<<"? "<<i<<" "<<i+1<<"\n";
fflush(stdout);
if(read()) {
id = i;
num++;
}
}
if(num>=2) { outing(1); continue; }
if(n&1) {
if(id) solve(id);
else {
cout<<"? "<<1<<" "<<n<<"\n";
fflush(stdout);
if(read()) solve(n);
else outing(1);
}
}
else {
if(!num) outing(1);
else solve(id);
}
}
return 0;
}
D:
构造一个序列,除了答案不能为0以外没有任何限制。
样例比较有吸引性,里面有很多我们熟悉的杨辉三角数,但想了想发现还是找不到关系,甚至无厘头。
一个正常的思路是:因为有一个式子是连乘,如果这个值比较大,我们会涉及到质因数分解之类的推导,这是比较麻烦的。因为不能有0,我们使其大部分为1。
我的解法中大部分的数对都是 2,-1 ,最后剩下一项时我们设成x,解一下方程就能得出 x=3-n。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <complex>
#include <queue>
#include <map>
using namespace std;
const ll N=801010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
typedef long double db;
inline ll read() {
ll sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
int T;
int n;
ll a[N],sum[N];
int main() {
T = read();
while(T--) {
n = read();
if(n==2) { cout<<"1 -3 -3 1\n"; continue; }
if(n==3) { cout<<"1 -10 6 6 -10 1\n"; continue; }
cout<<1<<" ";
for(int i=1;i<n;i++) cout<<"2 -1 ";
cout<<3-n<<"\n";
}
return 0;
}
G:
除了上下左右四个方向,蛇还可以缩尾巴(这使得蛇可以走到地图的任意位置),其实就是原地走一步,我们把原地走一步算成第五个方向。
用这五个方向进行BFS,复杂度显然是有问题的,因为我们允许原地走也就说明我们允许BFS中的某个状态满足它的步数不是当前位置的最短路,这样的话我们队列中的状态数会很多,因为我们每个位置都可以停留,状态平方级上升。
优化一下,一个重要的发现就是,我们原地走当且仅当我们被挡住了去路,否则原地踏步是没有意义的。
对于每个被挡住去路的地方,我们走到那里的最短时间只有两个可能的值:1、蛇尾巴离开的时间;2、蛇头走到那里的最短路。
于是,我们的BFS状态中就可以只允许出现最优状态了,当去路被挡住时,我们会把那个位置蛇尾离开的时间压入BFS状态中。这样的修改使得BFS中时间不是单调的了,我们得把队列修改为优先队列,BFS算法改成Dijkstra。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#define ull unsigned long long
#include <complex>
#include <queue>
#include <map>
using namespace std;
const ll N=3333;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
inline ll read() {
ll sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
int T;
int n,m,K;
char s[N];
int X[qwq],Y[qwq];
int a[N][N];
int dis[N][N];
int ff[] = {1,-1,0,0};
int gg[] = {0,0,1,-1};
struct D{
int x,y,di;
};
inline bool operator < (D A,D B) { return A.di > B.di; }
priority_queue <D> q;
int main() {
n = read(); m = read(); K = read();
for(int i=0;i<=n+1;i++) a[i][0] = a[i][m+1] = -1;
for(int j=0;j<=m+1;j++) a[0][j] = a[n+1][j] = -1;
for(int i=1;i<=K;i++) X[i] = read(), Y[i] = read();
for(int i=1;i<=n;i++) {
scanf("%s",s+1);
for(int j=1;j<=m;j++) a[i][j] = (s[j]=='.') ? 0 : -1, dis[i][j] = inf;
}
for(int i=1;i<=K;i++) {
int x = X[i], y = Y[i];
if(i==1) { q.push({x,y,0}); dis[x][y] = 0; }
a[x][y] = K-i+1;
}
while(!q.empty()) {
D now = q.top(); q.pop();
int x = now.x, y = now.y, di = now.di;
if(now.di!=dis[x][y]) continue;
for(int k=0;k<4;k++) {
int xx = x + ff[k];
int yy = y + gg[k];
if(a[xx][yy]==-1) continue;
if(a[xx][yy]<=di+1) {
if(dis[xx][yy] > di+1) {
dis[xx][yy] = di+1;
q.push({xx,yy,di+1});
}
}
else {
if(dis[xx][yy] > a[xx][yy]) {
dis[xx][yy] = a[xx][yy];
q.push({xx,yy,a[xx][yy]});
}
}
}
}
ull ans = 0;
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) if(dis[i][j]!=inf) ans += (ull)dis[i][j] * (ull)dis[i][j];
cout<<ans;
return 0;
}
H:
题目给出了一个基环森林,但是每个点访问顺序是随机的,我想了很久一个环上的顺序该如何处理,后来发现没必要,只要把所有小朋友分成三类就好。
第一类:\(a_i\ge a_{b_i}+w_{b_i}\) ,无论 \(b_i\) 那位小朋友有没有加糖,我都比 ta 多,我肯定不加糖。
第二类:\(a_{b_i}+w_{b_i} > a_i\ge a_{b_i}\),ta 加我就加,ta 没加我就不加。
第三类:\(a_{b_i}>a_i\) ,无论 ta 有没有加,我都比 ta 少,轮到我我就加。
分好这三类之后,我们将基环树上的点染成这三种颜色。
一个小朋友最终的期望值,取决于 ta 加糖的概率,所以第一类和第三类的答案已经固定了。
一个环上如果全都是第二类点,那么无论它们顺序如何,它们最终都不会加糖。第二类如果直接或间接指向第一类点,那么它也一定不会加。只有那些直接或间接指向第三类点的第二类点才需要我们去计算答案。可以发现这些结构是以第三类点为根的树,树上连的全是第二类点,没有了环的讨论。
树上的这些第二类点的答案如何计算,推到这里就比较简单了。
对于一个随机的排列中的两个值 a 和 b,a 在 b 的前面概率是 \(1/2\),说明深度为 1 的那些第二类点,概率就是 \(1/2\) ;对于随机排列中的三个值 a,b,c,a 在 b 前面,且 b 在 c 前面,概率是 \(1/6\) , 所以深度为 2 的第二类点概率是 \(1/6\)。结论就是,深度为 x 的第二类点,加糖概率为 \(\frac{1}{(n+1)!}\)。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <complex>
#include <queue>
#include <map>
using namespace std;
const ll N=801010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
const ll p=1000000007;
inline ll read() {
ll sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
ll T;
ll n;
ll a[N],b[N],w[N],cl[N];
ll ans[N];
ll f[N],ni[N];
vector <ll> e[N];
inline ll ksm(ll aa,ll bb) {
ll sum = 1;
while(bb) {
if(bb&1) sum = sum * aa %p;
bb >>= 1; aa = aa * aa %p;
}
return sum;
}
void chushihua() {
for(ll i=1;i<=n;i++) e[i].clear();
}
void qiu() {
f[0] = ni[0] = 1;
for(ll i=1;i<=N-10;i++) f[i] = f[i-1] * i %p;
ni[N-10] = ksm(f[N-10],p-2);
for(ll i=N-11;i>=1;i--) ni[i] = ni[i+1] * (i+1) %p;
}
void DFS(ll u,ll dep) {
(ans[u] += w[u] * ni[dep] %p) %= p;
FOR() {
ll v = e[u][i];
if(cl[v]==2) DFS(v,dep+1);
}
}
int main() {
qiu();
T = read();
while(T--) {
chushihua();
n = read();
for(ll i=1;i<=n;i++) a[i] = ans[i] = read();
for(ll i=1;i<=n;i++) b[i] = read();
for(ll i=1;i<=n;i++) w[i] = read();
for(ll i=1;i<=n;i++) {
e[b[i]].push_back(i);
if(a[i]>=a[b[i]]+w[b[i]]) cl[i] = 1;
else if(a[i]<a[b[i]]) cl[i] = 3;
else cl[i] = 2;
}
for(ll i=1;i<=n;i++) {
if(cl[i]==3) DFS(i,1);
}
for(ll i=1;i<=n;i++) cout<<(ans[i]%p+p)%p<<" \n"[i==n];
}
return 0;
}
E:
有趣的字符串题,但是数据范围也太卡人了,所以又没那么有趣了。
为了适应这个5e6的范围,我思考了很多修改,但为了好写最终还是选择了map版本,幸好一发过了,实现时有很多写的比较丑但是很无奈的地方(比如第65行)。
这题的思路是这样:
因为发现正着推不好推,我们倒着推:第 i+1 个字符串对第 i 个字符串造成了什么限制。
如果第 i 个字符串比第 i+1 个长,那么第 i 个字符串的前 \(|s_{i+1}|\) 位有哪些字母就被第 i+1 个串确定了,我们称之为限制。每个限制都有26个值,表示每个字母出现了几次,容易发现题目所给的输入其实就是每个字符串的初始限制。
倒着推,如果字符串长度越来越大,那么限制会越来越多,即每次加上最长字符串的初始限制,之前的限制依旧在,因为每一个字符串前面的几位都要保持一致。
如果第 i 个字符串比第 i+1 个短,就涉及到这题的关键了,因为上一个字符串是下一个的周期,因此我们可以计算出一个对于上一个字符串长度为 \(|s_{i+1}|\mod|s_i|\) 的限制,即按周期分块最后不够的那一部分,决定了上一个串的前几位。事实上对于下一个串的所有限制,都会对上一个串产生限制,产生的新限制的长度不会超过串本身长度。
然后到了令人恼火的复杂度分析阶段,本来这题已经做完了,但5e6的范围却不允许我们暴力记录每一个串的所有限制(因为要乘log),而是需要有一定的继承来降低复杂度。
我选用了一个map来记录当前串的所有限制。
对于上一个字符串长于下一个字符串的情况,下一个字符串的所有限制直接继承,并加入一个新的限制。
对于上一个字符串短于下一个字符串的情况,我们只需枚举长度大于短串的限制,计算出新限制插入回map,map可以去重。
第一个情况会向map中加入一个元素,而第二个情况不会增加,复杂度基本取决于map中元素个数。第二个情况如果频繁出现,map中元素个数不会超过串的长度,因此数据出现连续下降的小串(倒着看)不影响复杂度。如果出现连续上升的字符串,那么每次map中会多出一个元素,但是这种上升最多只会上升根号次,复杂度大致饱和于 \(\sqrt {5e6}*log*26\),理论可过,实际上也可以过。这个复杂度分析还是挺复杂的,对于更一般的情况可能需要势能分析,虽然这里只能给出大致的分析,但不影响做题。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <complex>
#include <queue>
#include <map>
using namespace std;
const ll N=301010;
const ll qwq=5030303;
const ll inf=0x3f3f3f3f;
const ll p=1000000007;
inline ll read() {
ll sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
ll T;
ll n;
char s[qwq];
int ans[qwq];
struct D{
int ch[26],he;
}a[N],ling,st[N];
int cnt;
map <int,D> f;
bool flag = 0;
void chushihua() {
f.clear();
for(int i=1;i<=n;i++) a[i] = ling;
flag = 0;
cnt = 0;
}
inline bool check(D A,D B) { // A has B
for(int i=0;i<26;i++) if(A.ch[i] < B.ch[i]) return !(flag=1);
return 1;
}
int main() {
T = read();
while(T--) {
chushihua();
n = read();
for(int i=1;i<=n;i++) {
scanf("%s",s);
a[i].he = strlen(s);
for(int j=0;j<a[i].he;j++) a[i].ch[s[j]-'a']++;
}
int lst = 0;
for(int i=n;i>=1;i--) {
if(a[i].he > lst) {
f[ a[i].he ] = a[i];
}
else {
for(auto it=--f.end(); ; it=--f.end()) {
if( (*it).first<=a[i].he ) break;
D now = (*it).second;
int ci = now.he/a[i].he;
for(int j=0;j<26;j++) {
now.ch[j] -= a[i].ch[j] * ci;
if(now.ch[j]<0) flag = 1;
}
if(flag) break;
now.he %= a[i].he;
st[++cnt] = now;
if( it==f.begin() ) {
f.erase(it);
break;
}
f.erase(it);
}
st[++cnt] = a[i];
for(int j=1;j<=cnt;j++) {
if(f.find(st[j].he)==f.end()) f[st[j].he] = st[j];
else check(f[st[j].he],st[j]);
}
cnt = 0;
}
if(flag) break;
lst = a[i].he;
}
D now = ling;
int id = 0;
for(auto it=f.begin();it!=f.end();it++) {
check((*it).second,now);
for(int i=0;i<26;i++)
for(int j=now.ch[i];j<(*it).second.ch[i];j++) ans[++id] = i;
now = (*it).second;
}
if(flag) {
cout<<"NO\n";
continue;
}
cout<<"YES\n";
for(int i=1;i<=id;i++) cout<<(char)(ans[i]+'a');
cout<<endl;
for(int i=2;i<=n;i++) {
for(int j=a[i].he;j>=1;j--) {
int shang = j % a[i-1].he;
if(shang==0) shang = a[i-1].he;
ans[j] = ans[shang];
}
for(int j=1;j<=a[i].he;j++) cout<<(char)(ans[j]+'a');
cout<<endl;
}
}
return 0;
}
F:
求树上某点k邻域内的mex。
一个比较有用的结论,也是这题的突破口:如果一个点的k邻域包含了一棵树直径的两个端点,那么这个k邻域一定包含了整个树,反之亦然。
所以,小于x的所有点构成的树,如果被这个k邻域包含了,那么这个k邻域一定包含了这棵树直径的两个端点。
我们考虑每一个答案是如何产生的,首先由0号点自己构成一棵树,如果它在k邻域内,就加入1号点,生成新树;不断地向内加点,直到数值不再连续,或者新生成的这棵树的直径端点突破了k邻域的范围,由于每个数只出现一次,因此这个突破k邻域的值就是mex。
从0号点开始增加的每一个值,我们都可以维护新树直径的两个端点。我们可以二分找到第一个突破k邻域的值,每次的check只需判断直径两个端点是否在k邻域内即可。
二分一个log,计算任意两点距离需要求LCA,又一个log,不幸地TLE了,我们只得把LCA改成O(1)的求法,但这题成为金牌题主要是因为思路,而不是这个小小的优化。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>
using namespace std;
const ll N=1101010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
ll n,Q;
struct D{
ll zhi,id;
}a[N];
inline bool cmp(D A,D B) { return A.zhi < B.zhi; }
struct E{
ll to,we;
};
vector <E> e[N];
ll dep[N];
ll f[N][23],rec[N][23];
ll id1[N],id2[N];
ll dfn[N],tot;
ll ob[N];
inline ll read() {
ll sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
void DFS(int u,int fa) {
f[++tot][0] = dep[u];
rec[tot][0] = u;
dfn[u] = tot;
FOR() {
int v = e[u][i].to, w = e[u][i].we;
if(v==fa) continue;
dep[v] = dep[u] + w;
DFS(v,u);
f[++tot][0] = dep[u];
rec[tot][0] = u;
}
}
inline int ask(int l,int r) {
int k = ob[r-l+1];
if(f[l][k] < f[r-(1<<k)+1][k]) return rec[l][k];
else return rec[r-(1<<k)+1][k];
}
inline int LCA(int x,int y) {
if(dfn[x]>dfn[y]) swap(x,y);
return ask(dfn[x],dfn[y]);
}
inline ll ju(ll u,ll v) { return dep[u] + dep[v] - 2 * dep[LCA(u,v)]; }
int main() {
ob[0] = -1; for(int i=1;i<=N-10;i++) ob[i] = ob[i>>1] + 1;
ll x,y,z;
n = read(); Q = read();
for(ll i=1;i<=n;i++) a[i].zhi = read(), a[i].id = i;
for(ll i=1;i<n;i++) {
x = read(); y = read(); z = read();
e[x].push_back({y,z});
e[y].push_back({x,z});
}
sort(a+1,a+n+1,cmp);
if(a[1].zhi!=0) {
while(Q--) cout<<"0\n";
return 0;
}
id1[1] = id2[1] = a[1].id;
DFS(a[1].id,0);
for(int k=1;k<=22;k++) {
for(int i=1;i+(1<<k-1)<=tot;i++) {
if(f[i][k-1] < f[i+(1<<k-1)][k-1])
f[i][k] = f[i][k-1], rec[i][k] = rec[i][k-1];
else
f[i][k] = f[i+(1<<k-1)][k-1], rec[i][k] = rec[i+(1<<k-1)][k-1];
}
}
ll da = 1;
for(ll i=2;i<=n;i++) {
if(a[i].zhi!=i-1) break;
da = i;
id1[i] = id1[i-1]; id2[i] = id2[i-1];
ll zhi = ju(id1[i],id2[i]);
ll ju1 = ju(id1[i],a[i].id);
ll ju2 = ju(id2[i],a[i].id);
if(ju1>zhi || ju2>zhi) {
if(ju1>ju2) id2[i] = a[i].id;
else id1[i] = a[i].id;
}
}
while(Q--) {
x = read(); z = read();
if(ju(x,id1[da])<=z && ju(x,id2[da])<=z) { cout<<da<<'\n'; continue; }
if(ju(x,id1[1])>z) { cout<<"0\n"; continue; }
ll l = 1, r = da-1, mid, ans;
while(l<=r) {
mid = l+r >> 1;
if(ju(x,id1[mid])<=z && ju(x,id2[mid])<=z) ans = mid, l = mid+1;
else r = mid-1;
}
cout<<ans<<endl;
}
return 0;
}
B:
多项式科技题。
首先题目输出答案的误差值就是把准确答案改为了区间是否有答案,若 [x,3x] 内有答案,我们输出1.5x即可。分段使复杂度多一个log。
若我们不考虑颜色,只考虑是否存在两个灯距离差为d,我们可以简单地进行卷积,将序列倒过来,i 变为 n+1-i,与原序列做卷积,若第n+1+d项有值,说明表示存在距离为d的两个灯,且数值为情况数。为了统计 [x,3x] 内是否有答案,我们让 a 序列只记录 [x,3x] 内的灯,b 序列记录所有位置的灯,把 a 倒过来和 b 卷积即可。
现在加上颜色:a 序列记录 [x,3x] 内灯的颜色,其他位置设成0(没有灯的位置也设成0),b 序列记录所有位置灯的颜色,如果存在一个 i 满足 i 位置的颜色和 i+d 位置不一样,那么可以得到:\(a_i-b_{i+d}\neq0\) ,即 \((a_i-b_{i+d})^2\neq 0\),将平方展开可以得到三项卷积的形式,一个是 a 序列的平方,一个是 ab 卷积,一个是 b 序列的平方,我们只需要求出这三项的和,判断 n+1+d 项是否为0。
要注意计算 a 或 b 序列的平方不能把所有位置都加起来,而是要满足 \(a_i\neq0\) 且 \(b_{i+d}\neq 0\),我们用 a1 和 b1 序列把所有不为 0 的位置设为1,再和平方项做卷积。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const ll N=1010101;
const ll inf=0x3f3f3f3f;
const ll p=998244353, g=3, gi=332748118;
ll Len;
ll r[N];
inline ll read() {
ll sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
inline ll ksm(ll aa,ll bb) {
ll sum = 1;
while(bb) {
if(bb&1) sum = sum * aa %p;
bb >>= 1; aa = aa * aa %p;
}
return sum;
}
inline ll init(ll wo) {
ll len = 1, L = 0; while(len<wo) len<<=1, L++;
for(ll i=0;i<len;i++) r[i] = (r[i>>1]>>1) | ((i&1)<<(L-1));
return len;
}
void NTT(ll *A,ll len,ll cl) {
for(ll i=0;i<len;i++) if(i<r[i]) swap(A[i],A[r[i]]);
for(ll k=1;k<len;k<<=1) {
ll g1 = ksm((cl==1)?g:gi, (p-1)/(k<<1));
for(ll j=0;j<len;j+=(k<<1)) {
ll gk = 1;
for(ll i=0;i<k;i++,gk=gk*g1%p) {
ll x = A[i+j], y = gk*A[i+j+k] %p;
A[i+j] = (x+y) %p; A[i+j+k] = (x-y+p) %p;
}
}
}
if(cl==1) return ;
ll inv = ksm(len,p-2);
for(ll i=0;i<len;i++) A[i] = A[i] * inv %p;
}
void PMUL(ll *F,ll *G) {
ll len = Len;
NTT(F, len, 1);
NTT(G, len, 1);
for(ll i=0;i<len;i++) F[i] = F[i] * G[i] %p;
NTT(F, len, -1);
}
ll n,Q,m;
ll a1[N],aa[N],a[N],b1[N],bb[N],b[N],c[N];
ll wei[N];
vector <ll> gg[N];
ll ans[N];
int main() {
ll x,y;
n = read(); Q = read();
for(ll i=1;i<=n;i++) {
x = read(); y = read(); wei[x] = i;
c[x] = y; m = max(m,x);
}
for(ll i=1;i<=Q;i++) {
x = read();
gg[x].push_back(i);
}
ll L = 1, R = min(2ll,n), zhong = 1;
while(1) {
for(ll i=1;i<=m;i++) {
if(c[i]) {
b[i] = c[i];
b1[i] = 1;
bb[i] = c[i] * c[i] %p;
if(wei[i]>=L && wei[i]<=R) {
a[m+1-i] = c[i];
a1[m+1-i] = 1;
aa[m+1-i] = c[i] * c[i] %p;
}
}
}
Len = init(m+m+1);
PMUL(aa,b1);
PMUL(a,b);
PMUL(a1,bb);
for(ll i=m+1;i<=2*m;i++) {
if((aa[i]-2*a[i]+a1[i]+2*p)%p) {
for(ll v : gg[i-m-1]) {
if(!ans[v]) ans[v] = zhong;
}
}
}
if(R==n) break;
L = R+1; zhong = R*3/2; R = min(R*3,n);
memset(a,0,sizeof(a));
memset(a1,0,sizeof(a1));
memset(aa,0,sizeof(aa));
memset(b,0,sizeof(b));
memset(b1,0,sizeof(b1));
memset(bb,0,sizeof(bb));
}
for(ll i=1;i<=Q;i++) cout<<ans[i]<<"\n";
return 0;
}
后记
在比赛的最后五分钟,电子科技大学 “UESTC_忒休斯之船” 队伍在第八发通过了 A 题,进入6题区,但由于罚时过高排在了六题倒数第二。
然而戏剧性的一幕发生了,滚榜过后,金牌线卡在了六题的倒一和倒二之间,我们学校这支队伍拿到了最后一场区域赛的最后一块金牌(之前合肥站的最后一块金牌也是我们学校拿了)。更有意思的是,比较悲惨的六题区唯一个没有拿到金牌的队伍,队名叫 “中山大学_教练我想拿金牌”。
这个赛季结束后,忒休斯之船队的两个学长退役,留下一个23级的大一选手,和我们队重组了队。忒休斯之船的本意就是一直换队友,可是这次是他加入我们,不能再叫忒休斯之船了哈哈。