2026年NOICA联盟-NOIP模拟赛(六)
A. 宇宙 (universe)
题目描述
\(n\times n\) 的网格,有 \(k\) 个星球,第 \(i\) 个星球质量为 \(m_i\),位于 \((x_i,y_i)\),它会产生引力场,与物理中不同,它产生的引力场是以回型一圈一圈扩散的。
具体地,一个位置 \((a,b)\) 受到星球 \(i\) 的引力大小为:\(max(0,m_i-max(|a-x_i|,|b-y_i|))\)。
求受到的引力大小之和小于等于 \(p\) 的位置的个数。
做法
一个星球的引力场可以看作 \(m\) 个以其所在位置为中心的正方形的叠加。将这些正方形二维差分一下,发现差分数组十分优美,从左上到右下的主对角线上都是加一,副对角线都是减一,其他地方为零。注意这个右下是原本最大的正方形右下角的右下一格。
我的做法有点蠢,看到切比雪夫距离没有思考直接暴力分讨去掉取最大值,具体来说就是对于某个点,以这个点为原点画两条线:\(y=x,y=-x\),将整个网格分成四个区域,每一区域中一种坐标的差一定不小于另一个坐标的差。
以左边的区域为例,其他区域可以旋转后同样处理。\(s_{x,y}\) 表示点 \((x,y)\) 的左边区域的星球对这个点的引力大小之和,整个网格以左上角为原点,左侧第一行为 \(x\) 轴,正方向向下,上面第一行为 \(y\) 轴,正方向向右,坐标从 \(1\) 开始标号。\(s_{x,y}=s_{x-1,y-1}+s_{x+1,y-1}-s{x,y-2}+v(x,y)+v(x,y-1)\),\(v(x,y)\) 表示在这个点的星球重量之和。但是这样明显是不对的,对于 \(s_{x-1,y-1}\) 要转移过来,要减去 \((y-1)-y_i>m\) 且在 \((x-1,y-1)\) 左边区域的星球个数,\(s_{x,y-2}\) 还要额外再减。
处理完这些发现有重,对于个点来说,这个点本身被加了 \(4\) 次,两条划分的直线各被加了 \(2\) 次,需要减去。那对每条斜线再做一次类似的,发现点本身被减成 \(0\) 次了,最后还要再加上。
还有这个寺阁玛出题人卡空间,由于 vector 空间消耗极大,旋转是不能额外开 vector 存,要直接交换旋转。
大份:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
typedef long long ll;
const int N=1e3+5;
int n,k,P;
vector<int>h[N][N];
int t[N][N];
vector<pair<int,int> >p[N];
int sum[N][N],num[N][N],s[N],ans[N][N],ANS[N][N];
void round(){
for(int i=1;i<=n/2;i++){
for(int j=i;j<=n-i;j++){
int x1=i,y1=j;
int x2=j,y2=n-i+1;
int x3=n-i+1,y3=n-j+1;
int x4=n-j+1,y4=i;
swap(t[x1][y1], t[x2][y2]);
swap(t[x2][y2], t[x3][y3]);
swap(t[x3][y3], t[x4][y4]);
swap(h[x1][y1], h[x2][y2]);
swap(h[x2][y2], h[x3][y3]);
swap(h[x3][y3], h[x4][y4]);
swap(ans[x1][y1], ans[x2][y2]);
swap(ans[x2][y2], ans[x3][y3]);
swap(ans[x3][y3], ans[x4][y4]);
}
}
}
void print(){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)cout<<ans[i][j]<<" ";cout<<"\n";
}cout<<"\n";
return;
}
void solve(){
for(int j=1;j<=n;j++){
for(int i=1;i<=n;i++)
for(int x:h[i][j])if(x+j<=n)p[x+j].push_back(make_pair(i,j));
if(j==1){
for(int i=1;i<=n;i++){
sum[i][j]=t[i][j];
num[i][j]=h[i][j].size();
ans[i][j]+=sum[i][j];
sum[i][j]-=num[i][j];
}
continue;
}
for(int i=1;i<=n;i++){
sum[i][j]=sum[i-1][j-1]+sum[i+1][j-1]-sum[i][j-2]+t[i][j]+t[i][j-1]-h[i][j-1].size();
num[i][j]=num[i-1][j-1]+num[i+1][j-1]-num[i][j-2]+h[i][j].size()+h[i][j-1].size();
if(i==1)
sum[i][j]+=sum[i][j-2]-sum[i-1][j-1],
num[i][j]+=num[i][j-2]-num[i-1][j-1];
if(i==n)
sum[i][j]+=sum[i][j-2]-sum[i+1][j-1],
num[i][j]+=num[i][j-2]-num[i+1][j-1];
ans[i][j]+=sum[i][j];
}
for(int i=1;i<=n;i++)s[i]=0;
for(auto x:p[j]){
s[max(1,x.first-(j-x.second))]++;
s[min(n,x.first+(j-x.second))+1]--;
}
for(int i=1;i<=n;i++)
s[i]+=s[i-1],
num[i][j]-=s[i],
sum[i][j]-=num[i][j];
for(int i=1;i<=n;i++)s[i]=0;
for(auto x:p[j]){
s[max(1,x.first-(j-1-x.second))]++;
s[min(n,x.first+(j-1-x.second))+1]--;
}
for(int i=1;i<=n;i++)
s[i]+=s[i-1],
num[i][j-1]-=s[i],
sum[i][j-1]-=num[i][j-1];
p[j].clear();
}
return;
}
pair<int,int> w[N];int cnt;
vector<int> e[N];
void solve2(){
int sum2=0,num2=0;
for(int i=0;i<=n;i++)s[i]=0;
for(int i=1;i<=cnt;i++){
for(int x:h[w[i].first][w[i].second])if(w[i].second+x<=n)s[w[i].second+x]++;
sum2+=t[w[i].first][w[i].second];
num2+=h[w[i].first][w[i].second].size();
ans[w[i].first][w[i].second]-=sum2;
num2-=s[w[i].second];
sum2-=num2;
}
return;
}
void work(){
for(int j=1;j<=n;j++){
cnt=0;
for(int i=1;i<=n-j+1;i++)w[++cnt]=make_pair(i,j+i-1);
solve2();
}
for(int i=2;i<=n;i++){
cnt=0;
for(int j=1;j<=n-i+1;j++)w[++cnt]=make_pair(i+j-1,j);
solve2();
}
return;
}
int main(){
freopen("universe.in","r",stdin);
freopen("universe.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>k>>P;
for(int i=1;i<=k;i++){
int x,y,m;cin>>x>>y>>m;
t[x][y]+=m;
h[x][y].push_back(m);
}
solve();work();round();
solve();work();round();
solve();work();round();
solve();work();round();
int Ans=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
ans[i][j]+=t[i][j];
if(ans[i][j]<=P)Ans++;
}
cout<<Ans;
return 0;
}
B. 翻牌很爽(cards)
题目描述
有两个数组 \(a,b\),可以交换任意多次 \(a_i,b_i\),求交换完后两个数组的 mex 的和的最大值。
做法
看到选择两个数中一个放序列 \(a\) 一个放序列 \(b\),想到连边。
观察一下,发现只出现了一次的数肯定只能在一个序列中,找到最小的只出现了一次的数,假设分配给序列 \(a\),那么序列 \(b\) 的 mex 不超过这个数。
将 \(a_i,b_i\) 连边,将分配转化成给无向边定向,被指向的点分配给序列 \(a\)。将连通块分类,对于边数大于等于点数的连通块一定可以找到一个环,对于环上的所有边定同一个方向一定最优,因为所有数都同时属于两个序列。环外的边和点怎么办呢,断掉环上的边,随便找一棵生成树,每棵生成树找环上的点作为根,所有边指向叶子的方向,这样的构造使这个连通块中所有点都在序列 \(a\) 中,所有非叶子节点都在序列 \(b\) 中,刚好序列 \(b\) 已经不选了最小的出现次数为 \(1\) 的数,选不选叶子没有影响。
对于树连通块,无论怎么定向,树上一定有一个点不在序列 \(a\) 中,那就贪心选择最大的数不在序列 \(a\) 中。将不在序列 \(a\) 中的数作为根,边方向定为指向叶子,那么序列 \(b\) 同时也是最优的,因为只有叶子没选入序列 \(b\)。
实现
整理一下思路,现在就是要求只出现了一次的数的最小值,和树连通块中最大的点的最小值。加起来就是答案。没出现的数就是单点,也是树。但是序列 \(b\) 的最大值也不能超过没出现过的的数,所以序列 \(b\) 的答案是出现次数小于等于 \(1\) 次的最小数。并查集连边的时候,如果两个点在同一个连通块内,就将这个连通块的最大值设为正无穷,否则取两个连通块的较大值作为新连通块的最大值。
一个疑问
数的大小很大数组开不下怎么办?可以直接对 \(n+Q\) 取较小值。这样是不会影响答案的,即不会出现原本答案是 \(n+Q\) 但是这样做就变成了 \(n+Q+1\),因为对于序列 \(a\),最大值要是 \(n+Q+1\),小于等于 \(n+Q\) 的点之间就至少有 \(n+Q+1\) 条边(因为 \(0\) 也是一个点),这显然是不可能的。对于序列 \(b\),mex 要是 \(n+Q+1\),小于等于 \(n+Q\) 的数都要出现两次,总共出现次数至少为 \(2\times(n+Q+1)\)(\(0\) 也要出现两次),而一条边只能提供两个出现次数,至少要 \(n+Q\) 条边,不行。
原题需要动态加入和删除,套个线段树分治和可撤销并查集就行了。
注意线段树分治要用 multiset 存出现过的操作。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<set>
#include<queue>
using namespace std;
typedef long long ll;
typedef multiset<pair<pair<int,int>,int> > mtst;
const int N=2e5+5,inf=1061109567;
bool FI;
int n,Q,num,a[N],b[N],d[N<<2],ans[N];
struct ques{
char op;int a,b;
}q[N];
struct bcj{
int fa,siz,maxn;
}f[N<<2],g[N<<2];int st[N<<2],tp;
priority_queue<int,vector<int>,greater<int> >q1,q2,q3,q4;
int Find(int u){return f[u].fa==u?u:Find(f[u].fa);}
void Clear(){
while(!q1.empty()&&!q2.empty()&&q1.top()==q2.top())q1.pop(),q2.pop();
while(!q3.empty()&&!q4.empty()&&q3.top()==q4.top())q3.pop(),q4.pop();
return;
}
void Merge(int u,int v){
if(d[u]==1)q4.push(u);d[u]++;
if(d[v]==1)q4.push(v);d[v]++;
u=Find(u),v=Find(v);
if(f[u].siz>f[v].siz)swap(u,v);
st[++tp]=u;g[tp]=f[v];
Clear();
if(u==v)return q2.push(f[u].maxn),q1.push(inf),f[u].maxn=inf,void();
f[u].fa=v;
if(f[v].maxn>=f[u].maxn)q2.push(f[u].maxn);
else q2.push(f[v].maxn),f[v].maxn=f[u].maxn;
f[v].siz+=f[u].siz;
return;
}
void undo(){
if(f[st[tp]].fa==st[tp])q2.push(inf),q1.push(g[tp].maxn);
else if(g[tp].maxn==f[f[st[tp]].fa].maxn)q1.push(f[st[tp]].maxn);
else q1.push(g[tp].maxn);
Clear();
f[f[st[tp]].fa]=g[tp];
f[st[tp]].fa=st[tp];
if(f[st[tp]].fa!=st[tp])f[f[st[tp]].fa].siz-=f[st[tp]].siz;
tp--;
return;
}
mtst s;
vector<pair<int,int> >o[N<<2];
void update(int id,int l,int r,int x,int y,pair<int,int> v){
if(x>y)return ;
if(l>=x&&r<=y)return o[id].push_back(v),void();
int mid=(l+r)>>1,ls=id<<1,rs=ls|1;
if(x<=mid)update(ls,l,mid,x,y,v);
if(y>=mid+1)update(rs,mid+1,r,x,y,v);
return;
}
void solve(int id,int l,int r){
if(l>r)return;
int mem=tp;
for(auto x:o[id])Merge(x.first,x.second);
if(l==r){
Clear();
ans[l]=q1.top()+q3.top();
while(tp>mem)undo();
for(auto x:o[id]){
d[x.first]--;if(d[x.first]==1)q3.push(x.first);
d[x.second]--;if(d[x.second]==1)q3.push(x.second);
}
return;
}
int mid=(l+r)>>1,ls=id<<1,rs=ls|1;
solve(ls,l,mid);solve(rs,mid+1,r);
while(tp>mem)undo();
for(auto x:o[id]){
d[x.first]--;if(d[x.first]==1)q3.push(x.first);
d[x.second]--;if(d[x.second]==1)q3.push(x.second);
}
return;
}
bool SE;
int main(){
// freopen("P16458.in","r",stdin);
// freopen("P16458.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>Q;
for(int i=1;i<=n;i++){
cin>>a[i]>>b[i];
a[i]=min(a[i],n+Q);
b[i]=min(b[i],n+Q);
}
Q++;
for(int i=2;i<=Q;i++){
cin>>q[i].op>>q[i].a>>q[i].b;
q[i].a=min(q[i].a,n+Q-1);
q[i].b=min(q[i].b,n+Q-1);
}
for(int i=0;i<n+Q;i++)
f[i]=(bcj){i,1,i},
q1.push(i),
q3.push(i);
q3.push(inf);
mtst::iterator it;
for(int i=1;i<=n;i++)
s.insert(make_pair(make_pair(a[i],b[i]),1));
for(int i=2;i<=Q;i++){
if(q[i].op=='+')s.insert(make_pair(make_pair(q[i].a,q[i].b),i));
else{
it=s.lower_bound(make_pair(make_pair(q[i].a,q[i].b),0));
update(1,1,Q,(*it).second,i-1,(*it).first);
s.erase(it);
}
}
for(auto x:s)update(1,1,Q,x.second,Q,x.first);
solve(1,1,Q);
for(int i=1;i<=Q;i++)cout<<ans[i]<<"\n";
return 0;
}
C. 数学(math)
被 T1 的做法坑惨了,已经无法思考了。
题目描述
有大小为 \(n\) 的数集,求所有子集乘积的欧拉函数之和。
做法
对于一个数的欧拉函数,唯一特殊的在于一种质数初次出现的时候是乘 \(p-1\),那么显然要维护出现的质数的集合。由于质数太多了直接状压会爆炸,发现一个数,大于根号 \(V\) 的质因子至多只有 \(1\) 个,那么对于质数非常多的大于根号 \(V\) 的部分,一个数最多只会把一个位置改为 \(1\),这样就太浪费状态了。按大质数分类,每类分别 dp,再整一类贡献给全局的 dp,这样就只需要对小于根号 \(V\) 的质数状态压缩,开的下了。
dp
设 \(f_{i,S}\) 表示前 \(i\) 个数,选择的数的小质数集合为 \(S\),的所有方案的乘积之和。当然由于我们已经将每个数分类了,这里的前 \(i\) 个数并不一定是原序列的前 \(i\) 个数。欧拉函数的一种求法是,如果 \(x\) 被质因数分解之后是 \(p_1^{e_1}p_2^{e_2}\dots p_m^{e_m}\),那么 \(\varphi(x)=x\times\prod_{1\leq i\leq m}\dfrac{p_i-1}{p_i}\)。当我们知道小质数的集合时,连乘那一部分相当于是固定了,所以 dp 只需要记录乘积(\(x\))和,当然大质数特殊处理,还要乘上大质数 \(P\) 的 \(\dfrac{P-1}{P}\)。
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
typedef long long ll;
const int N=2005,V=3005,mod=998244353;
int add(int a){return a>=mod?a-mod:a;}
int ksm(int a,int b){int ans=1;while(b){if(b&1)ans=1ll*ans*a%mod;a=1ll*a*a%mod;b>>=1;}return ans;}
int n,f[2][(1<<16)+114],now=1,pre,g[2][(1<<16)+114];
vector<int>h[V];
bool isprime[V];int prime[V],inv[V],cnt;
void init(){
isprime[0]=isprime[1]=1;
for(int i=2;i<V;i++){
if(!isprime[i])
prime[++cnt]=i,
inv[cnt]=ksm(i,mod-2);
for(int j=1;j<=cnt;j++){
if(i*prime[j]>=V)break;
isprime[i*prime[j]]=1;
if(i%prime[j]==0)break;
}
}
return;
}
int getv(int x){
int S=0;
for(int i=1;i<=16;i++)if(x%prime[i]==0)S|=1<<i-1;
return S;
}
int main(){
// freopen("math.in","r",stdin);
// freopen("math.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n;
init();
for(int i=1;i<=n;i++){
int w,v;
cin>>w;v=w;
for(int j=1;j<=16;j++)if(w%prime[j]==0){
while(w%prime[j]==0)w/=prime[j];
}
h[w].push_back(v);
}
f[now][0]=1;
for(int x:h[1]){
int T=getv(x);
swap(now,pre);
for(int S=0;S<(1<<16);S++)
f[now][S]=add(f[now][S]+f[pre][S]),
f[now][S|T]=add(f[now][S|T]+1ll*f[pre][S]*x%mod);
for(int S=0;S<(1<<16);S++)f[pre][S]=0;
}
for(int i=17;i<=cnt;i++){
int q=now;
now=1;pre=0;
for(int S=0;S<(1<<16);S++)
g[now][S]=f[q][S],
g[pre][S]=0;
for(int x:h[prime[i]]){
int T=getv(x);
swap(now,pre);
for(int S=0;S<(1<<16);S++)
g[now][S]=add(g[now][S]+g[pre][S]),
g[now][S|T]=add(g[now][S|T]+1ll*g[pre][S]*x%mod);
for(int S=0;S<(1<<16);S++)g[pre][S]=0;
}
swap(now,q);pre=now;now^=1;
for(int S=0;S<(1<<16);S++)
f[now][S]=add(1ll*(prime[i]-1)*inv[i]%mod*g[q][S]%mod+1ll*inv[i]*f[pre][S]%mod),
f[pre][S]=0;
}
int ans=0;
for(int S=0;S<(1<<16);S++){
int Inv=1;
for(int i=1;i<=16;i++)if((S>>i-1)&1)Inv=1ll*(prime[i]-1)*inv[i]%mod*Inv%mod;
ans=add(ans+1ll*f[now][S]*Inv%mod);
}
cout<<ans;
return 0;
}

浙公网安备 33010602011771号