hijklmn
黑暗料理
题意简述:给定一个长度为 \(n\) 的序列,删掉一些数,使得在剩下的序列中任选两个数 \(x,y\) 使得 \(x+y\) 都为非质数,求最多剩下多少数。\((n\le 750)\)
首先显然最终剩下的序列中最多有一个 \(1\)。
再考虑到所有偶数都不是质数,可以把序列分成奇数和偶数两部分,用 Miller-Rabin 判断两数之和是否为质数,若是则将这两个数连边,得到的其实是一张二分图。
那么就转换为选最多的点,满足两两之间没有边相连。也就是求这张二分图的最大独立集。
点击查看代码
#include<bits/stdc++.h>
using ll=long long;
const int inf=0x3f3f3f3f;
inline ll qpow(ll a,ll b,ll p){ll ans(1);for(;b;b>>=1,a=a*a%p)if(b&1)ans=ans*a%p;return ans;}
const short prime[]={2,7,61};
inline bool check(int x,int a){
int d=x-1,t=qpow(a,d,x);
if(t!=1)return false;
for(d>>=1;!(d&1);d>>=1){
t=qpow(a,d,x);
if(t==x-1)return true;
if(t!=1)return false;
}
return true;
}
inline bool MR(int x){
if(x==2||x==7||x==61)return true;
if(!(x&1))return false;
for(int a:prime)if(!check(x,a))return false;
return true;
}
int n,s,t,a[800];
struct Maxflow{
struct node{int to,next,flow;}a[350000];
int o=1,head[800],cur[800],dis[800],q[800],hd,tl;
inline void clear(){o=1;memset(head,0,sizeof head);memset(cur,0,sizeof cur);memset(a,0,sizeof a);}
inline void add(int x,int y,int c){
a[++o]=(node){y,head[x],c};head[x]=o;
a[++o]=(node){x,head[y],0};head[y]=o;
}
inline bool bfs(){
for(int i=0;i<=t;++i)cur[i]=head[i],dis[i]=0;
q[hd=tl=1]=s,dis[s]=1;
while(hd<=tl){
int x=q[hd++];
for(int i=head[x],y;i;i=a[i].next){
if(a[i].flow&&!dis[y=a[i].to]){
dis[y]=dis[x]+1;
q[++tl]=y;
if(y==t)return true;
}
}
}
return false;
}
int dfs(int x,int flow){
if(!flow||x==t)return flow;
int fl,tmp=0;
for(int i=cur[x],y;i;i=a[i].next){
cur[x]=i;
if(a[i].flow&&dis[y=a[i].to]==dis[x]+1&&(fl=dfs(y,std::min(flow-tmp,a[i].flow)))){
a[i].flow-=fl,a[i^1].flow+=fl,tmp+=fl;
if(!(flow-tmp))break;
}
}
return tmp;
}
inline int dinic(){
int maxflow=0;
while(bfs())maxflow+=dfs(s,inf);
return maxflow;
}
}fl;
namespace Koishi{
static int p1[800],p2[800];
inline void solve(){
fl.clear();
std::cin>>n;
int cnt1=0,cnt2=0;
for(int i=1;i<=n;++i)std::cin>>a[i];
std::sort(a+1,a+1+n);
if(a[1]==1)p1[++cnt1]=1;
for(int i=1;i<=n;++i){
if(a[i]==1)continue;
if(a[i]&1)p1[++cnt1]=a[i];
else p2[++cnt2]=a[i];
}
s=0,t=cnt1+cnt2+1;
for(int i=1;i<=cnt1;++i)fl.add(s,i,1);
for(int i=1;i<=cnt2;++i)fl.add(cnt1+i,t,1);
for(int i=1;i<=cnt1;++i){
for(int j=1;j<=cnt2;++j){
if(MR(p1[i]+p2[j]))fl.add(i,cnt1+j,inf);
}
}
std::cout<<(cnt1+cnt2-fl.dinic())<<'\n';
}
}
int main(){
freopen("cooking.in","r",stdin);freopen("cooking.out","w",stdout);
std::cin.tie(nullptr)->sync_with_stdio(false);
int T;std::cin>>T;while(T--)Koishi::solve();
return 0;
}
害怕
题意简述: 有 \(n\) 个点 \(m\) 条边的无向图,每条边为蓝色或白色,要给每条边赋上边权,边权为 \(1\sim m\) 的排列,要求蓝色的边为图的最小生成树(保证有解),求字典序最小的一组解。\((n,m\le 5\times 10^5)\)
贪心地去考虑,从前往后依次处理每条边,把边权小的尽可能往前放。若当前边是蓝边,就直接赋值。若当前边是白边,要满足蓝边形成 MST,所以要保证这条白边与蓝边形成的环中,白边的权值是最大的。那么就要把夹在中间尚未赋值的蓝边按编号依次赋值,再把白边赋上值。暴力跳是 \(O(n^2)\) 的。考虑到赋过值的边没有作用,就可以用并查集把树上已赋值的边缩起来,可以做到 \(O(n\alpha(n))\)。(挂分原因:注意到可能有重边,不能直接用两个端点代表一条边)
点击查看代码
#include<bits/stdc++.h>
#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/hash_policy.hpp>
using ll=long long;
using ull=unsigned long long;
int n,m,cnt;
struct edge{int u,v,c;}e[500005];
std::vector<int> g[500005],tmpid,tmpval;
std::queue<int> q;
__gnu_pbds::gp_hash_table<int,ull> h;
__gnu_pbds::gp_hash_table<ull,int> p;
std::mt19937_64 rnd(std::chrono::steady_clock::now().time_since_epoch().count());
int ans[500005],fa[500005],dep[500005],F[500005],root;
int getfa(int x){return F[x]==x?x:F[x]=getfa(F[x]);}
void dfs(int x,int f){
dep[x]=dep[f]+1,fa[x]=f;
for(int &y:g[x]){
if(y==f)continue;
dfs(y,x);
}
}
int main(){
std::cin.tie(nullptr)->sync_with_stdio(false);
std::cin>>n>>m;
for(int i=1;i<=n;++i)h[i]=rnd();
for(int i=1,u,v,c;i<=m;++i){
q.push(i);
std::cin>>u>>v>>c;
p[h[u]*h[v]+c]=i;
e[i]=(edge){u,v,c};
if(c==1){
if(!root)root=u;
g[u].push_back(v);
g[v].push_back(u);
}
}
std::iota(F+1,F+1+n,1);
dfs(root,0);
for(int i=1;i<=m;++i){
int x=e[i].u,y=e[i].v;
int fx=getfa(x),fy=getfa(y);
if(e[i].c==1){
if(!ans[i])ans[i]=q.front(),q.pop();
if(fx!=fy){
if(dep[fx]>dep[fy])std::swap(fx,fy);
F[fy]=fx;
}
}
else{
tmpid.clear();
tmpval.clear();
while(fx!=fy){
if(dep[fx]<dep[fy])std::swap(x,y),std::swap(fx,fy);
tmpid.push_back(p[h[fx]*h[fa[fx]]+1]);
tmpval.push_back(q.front());
q.pop();
x=fx,fx=getfa(fa[x]);
F[x]=fx;
}
std::sort(tmpid.begin(),tmpid.end());
for(int j=0;j<tmpid.size();++j)ans[tmpid[j]]=tmpval[j];
ans[i]=q.front();q.pop();
}
}
for(int i=1;i<=m;++i)std::cout<<ans[i]<<' ';
return 0;
}
博弈树
题意简述:给一棵 \(n\) 个点的树,A 和 B 轮流移动一颗棋子到树上任意位置,要求每次移动要比对方上一次移动距离大,A先手。\(q\) 次询问,每次询问给定起点 \(u\),问谁会获胜。\((n,q\le 10^5)\)
树上任意两点最长的距离是树的直径,所以一方如果在直径端点上,只要移动到另一个端点,就一定可以获胜。考虑将不断将所有直径端点一层层删去,最后剩下的那个点是先手必败的,因为它无论如何都会移动到直径端点上去,其他情况都是先手必胜。而剩下的这个点就是直径中点。
点击查看代码
#include<bits/stdc++.h>
int n,q;
std::vector<int> g[100005];
int dep[100005],son[100005],siz[100005],fa[100005],m1,m2,maxd;
void dfs1(int x,int f){
dep[x]=dep[f]+1,siz[x]=1,fa[x]=f;
if(dep[m1]<dep[x])m1=x;
for(int &y:g[x]){
if(y==f)continue;
dfs1(y,x);
siz[x]+=siz[y];
if(siz[y]>siz[son[x]])son[x]=y;
}
}
int top[100005],id[100005],tot;
void dfs2(int x,int t){
top[x]=t,id[x]=++tot;
if(!son[x])return;
dfs2(son[x],t);
for(int &y:g[x])if(y!=fa[x]&&y!=son[x])dfs2(y,y);
}
void dfs3(int x,int f,int d){
if(maxd<=d)maxd=d,m2=x;
for(int &y:g[x]){
if(y==f)continue;
dfs3(y,x,d+1);
}
}
inline int lca(int x,int y){
while(top[x]^top[y]){
if(dep[top[x]]<dep[top[y]])std::swap(x,y);
x=fa[top[x]];
}
return dep[x]<dep[y]?x:y;
}
inline int getdist(int x,int y){return dep[x]+dep[y]-2*dep[lca(x,y)];}
int main(){
freopen("tree.in","r",stdin);freopen("tree.out","w",stdout);
std::cin.tie(nullptr)->sync_with_stdio(false);
std::cin>>n>>q;
for(int i=1,u,v;i<n;++i)std::cin>>u>>v,g[u].push_back(v),g[v].push_back(u);
dfs1(1,0);dfs2(1,1);dfs3(m1,0,0);
std::cerr<<maxd<<' '<<m1<<' '<<m2<<'\n';
for(int u;q;--q){
std::cin>>u;
int dis=std::max(getdist(u,m1),getdist(u,m2));
if(dis>maxd/2)std::cout<<"Alice\n";
else std::cout<<"Bob\n";
}
return 0;
}
划分
题意简述:长度为 \(n\) 的 01 串 \(S\),求将其分成至少 \(k\) 段,把每一段看成二进制数求和后的最大值,以及取到这个最大值的划分数量。
首先想到要尽可能最大化某一段的长度,这样得到的值越大。分情况讨论,若前导零个数满足大于等于 \(k\),则最大值一定是后面分成的一整个大段,方案数就是把前面 \(cnt\) 个 0 分成大于等于 \(k\) 段的方案数。插板法可以求得答案为 \(\sum_{i\ge k} {cnt\choose i-1}\)。再考虑其他情况,我们希望把序列分成 \(k-1\) 段长度的和一段长度为 \(n-k+1\) 的,设其为 \(p\)。最终答案就为 \(p+\text{popcount}(S)-\text{popcount}(p)\)。我们的目标就是最大化长度为 \(n-k+1\) 的段,可以二分哈希快速比较,复杂度 \(O(n\log n)\)。注意当 \(p\) 末位是 1 时,\(p-1\) 也为一种合法的划分,因为会和 \(\text{popcount}(p)\) 抵消贡献,不要漏算方案数。
点击查看代码
#include<bits/stdc++.h>
using ll=long long;
const int P=998244353;
int n,k;
int fac[2000005],inv[2000005];
inline ll qpow(ll a,ll b){ll ans(1);for(;b;b>>=1,a=a*a%P)if(b&1)ans=ans*a%P;return ans;}
inline ll C(ll x,ll y){return x<y?0:(ll)fac[x]*inv[y]%P*inv[x-y]%P;}
char s[2000005];
ll h[2000005],base[2000005];
inline ll gethash(int x,int len){return (h[x+len-1]-h[x-1]*base[len]%P+P)%P;}
int main(){
freopen("divide.in","r",stdin);freopen("divide.out","w",stdout);
std::cin.tie(nullptr)->sync_with_stdio(false);
std::cin>>n>>k;
for(int i=1;i<=n;++i)std::cin>>s[i];
int p=1,sum=0,cnt=0;
if(n==k){
for(int i=1;i<=n;++i)sum+=(s[i]=='1');
std::cout<<sum<<' '<<1;
exit(0);
}
fac[0]=inv[0]=1;
for(int i=1;i<=n;++i)fac[i]=(ll)fac[i-1]*i%P;
inv[n]=qpow(fac[n],P-2);
for(int i=n-1;i;--i)inv[i]=(ll)inv[i+1]*(i+1)%P;
while(p<=n&&s[p]=='0')p++;
if(p>k){
for(int i=p;i<=n;++i)sum=(2ll*sum%P+(s[i]=='1'))%P;
if(p==n+1)p--;
for(int i=k-1;i<p;++i)cnt=(cnt+C(p-1,i))%P;
std::cout<<sum<<' '<<cnt;
exit(0);
}
for(int i=1;i<=n;++i)h[i]=(h[i-1]*131%P+s[i])%P;
base[0]=1;
for(int i=1;i<=n;++i)base[i]=base[i-1]*131%P;
int now=1;cnt=1;
for(int i=2;i<=k;++i){
int l=0,r=n-k+1,mid,ans;
while(l<=r){
mid=l+r>>1;
if(gethash(now,mid)==gethash(i,mid))l=mid+1,ans=mid;
else r=mid-1;
}
if(ans>=n-k){
cnt++;
continue;
}
if(s[now+ans]<s[i+ans]){
now=i;
cnt=1;
}
}
for(int i=now;i<=now+n-k;++i)sum=(sum*2%P+(s[i]=='1'))%P;
for(int i=1;i<now;++i)sum+=(s[i]=='1');
for(int i=now+n-k+1;i<=n;++i)sum+=(s[i]=='1');
sum%=P;
std::cout<<sum<<' '<<cnt;
return 0;
}
旅行
题意简述:\(n\) 个点 \(n\) 条边的无向连通图每条边有初始颜色,\(m\) 次操作 \(x,y,c\) 表示将连接 \(x,y\) 的边染成 \(c\) 颜色(保证连接 \(x,y\) 的边是原图的边),求每次操作之后有多少颜色相同的连通块。\((n,m\le 10^5)\)
初始图是一棵基环树。我们可以用哈希表维护每个点的出边颜色种类及个数。染色修改操作可以看成先删后加,这样就可以轻松维护连通块数量。但还需要特殊处理环上的边,若环上都为一种颜色时,需要额外加上多减去的 1。
点击查看代码
namespace Flandreqwq{
int n,m,o,sum[100005],head[100005],dep[100005],stk[100005],cir[100005],iscir[200005],top,cnt,ans;
hash_table col[100005],id[100005];
struct E{int to,next,w;}e[200005];
inline void add(int x,int y,int z){
e[++o]={y,head[x],z};head[x]=o;
id[x][y]=o;
}
void dfs(int x,int f){
dep[x]=dep[f]+1;
stk[++top]=x;
for(int i=head[x],y;i;i=e[i].next){
if((y=e[i].to)==f)continue;
if(dep[y]&&dep[y]<dep[x]){
while(stk[top]!=y)cir[++cnt]=stk[top--];
cir[++cnt]=y;
}
else if(!dep[y]){
dfs(y,x);
}
}
if(stk[top]==x)top--;
}
inline void clear(){
for(int i=1;i<=n;++i){
col[i].clear();
id[i].clear();
sum[i]=head[i]=dep[i]=0;
}
for(int i=1;i<=o;++i)iscir[i]=0;
o=1,cnt=0,top=0,ans=0;
}
inline void ins(int x,int y,int c,int pd){
if(!col[x][c]&&!col[y][c])ans++;
if(col[x][c]&&col[y][c])ans--;
col[x][c]++,col[y][c]++;
if(pd)sum[c]++;
if(pd&&sum[c]==cnt)ans++;
}
inline void del(int x,int y,int c,int pd){
col[x][c]--,col[y][c]--;
if(!col[x][c]&&!col[y][c])ans--;
if(col[x][c]&&col[y][c])ans++;
if(pd&&sum[c]==cnt)ans--;
if(pd)sum[c]--;
}
inline void solve(){
clear();
std::cin>>n>>m;
for(int i=1,x,y,c;i<=n;++i){
std::cin>>x>>y>>c;
add(x,y,c),add(y,x,c);
}
dfs(1,0);
for(int i=1;i<=cnt;++i)iscir[id[cir[i]][cir[i%cnt+1]]]=iscir[id[cir[i]][cir[i%cnt+1]]^1]=1;
for(int i=2;i<=o;i+=2)ins(e[i].to,e[i^1].to,e[i].w,iscir[i]);
for(int i=1,x,y,c;i<=m;++i){
std::cin>>x>>y>>c;
int p=id[x][y];
del(x,y,e[p].w,iscir[p]);
ins(x,y,c,iscir[p]);
e[p].w=e[p^1].w=c;
std::cout<<ans<<'\n';
}
}
void main(){
int testcases;
std::cin>>testcases;
while(testcases--)solve();
}
}
异或
题意简述:给定长度为 \(n\) 的数组 \(a\),每次选一个区间 \([l,r]\),将 \(a_{[l,r]}\) 异或上 \(w\),求将 \(a_i\) 全变为 0 的最少操作数。\((n\le 17,0 \le a_i\le 10^{18})\)
求出 \(a\) 的差分序列 \(b\),这样就将区间操作转化为了对差分序列的双点操作或者单点操作(取后缀)。最终 \(b\) 序列也会全变为 0。考虑将每次修改位置连边,最终会得到若干个连通块,设连通块大小为 \(x\),则其边数上界为 \(x\)(一堆双点操作加上一个单点一定行),下界为 \(x-1\)。而且取到下界时当且仅当连通块内的数异或和为 0。必要性:考虑到一定都是双点操作,连通块内的异或和不会因操作发生改变,最后是 0 所以初始一定是 0。充分性:每次异或掉开头的数,\(x-1\) 次操作后最后一定剩下 0。设会有 \(y\) 个连通块边数取到下界,那么答案就是 \(n-y\)。我们就要最大化 \(y\),问题转换为,我们需要求差分序列异或和为 0 的子序列的最多个数。状压后枚举子集转移即可 \(f_s=f_{s \oplus t}+1 (t\subset s \wedge \text{t 内异或和为 0})\)。时间复杂度 \(O(3^n)\)。
点击查看代码
std::cin>>n;
for(int i=1;i<=n;++i)std::cin>>a[i];
for(int i=1;i<=n;++i)b[i]=a[i]^a[i-1];
maxn=1<<n;
for(int s=1;s<maxn;++s){
ll sum=0;
for(int i=1;i<=n;++i){
if(s&(1<<i-1))sum^=b[i];
}
if(!sum)f[s]=true;
}
for(int s=1;s<maxn;++s){
for(int t=s;t;t=(t-1)&s){
if(f[t])g[s]=std::max(g[s],g[s^t]+1);
}
}
std::cout<<n-g[maxn-1];
差后队列
题意简述:一个队列,支持两种操作,正常插入一个数,随机删除一个不是最大值的数(若只有一个数就删除该数),对于每个插入操作求出该数期望在哪一步被删除的,对于每个删除操作求出被删除的期望大小。
不是最大值的数是等价的。从前往后处理如果一个数不是最大值就把它加入贡献,维护删掉数后的元素个数为 \(siz\),遇到删除操作答案就为\(\frac{sum}{siz}\),之后 \(sum\to 1-\frac{sum}{siz}\)。删除操作成功删掉一个非最大值的概率是 \(\frac{1}{siz}\)。对于一个数,它被删除的时间期望是在后面删除操作上被删除的期望和,这个数在一个删除操作还要考虑后面的数都没被删掉的概率,从后往前处理并更新答案即可。时间复杂度 \(O(n)\)。
点击查看代码
std::cin>>n;
for(int i=1,opt;i<=n;++i){
std::cin>>opt;
if(!opt){
cnt++;
std::cin>>a[i];
if(mx==0)mx=i;
else if(a[mx]<a[i]){
Modadd(sum,a[mx]);
pre[i]=mx;
mx=i;
}
else{
Modadd(sum,a[i]);
pre[i]=i;
}
}
else{
cnt--;
if(cnt){
Modmul(sum,Inv(cnt));
ans[i]=sum;
Modmul(sum,cnt-1);
}
else{
ans[mx]=i;
ans[i]=a[mx];
mx=0;
}
}
siz[i]=cnt;
}
int k=0;
for(int i=n;i;--i){
if(a[i]){
ans[pre[i]]=k;
}
else{
k=Add(Mul(i,Inv(siz[i])),Mul(k,Dec(1,Inv(siz[i]))));
}
}
for(int i=1;i<=n;++i)std::cout<<ans[i]<<' ';
高爸
题意简述:给定 \(n\) 个数的序列 \(a\),你有两种操作,使 \(a_i\) 加上 1 或者减去 1,代价均为 1。对于每一个 \(i\),求使得序列前 \(i\) 个数都相等的最小花费。
可以枚举最终可能变为的数,取最小值,这样是 \(O(n^2)\) 的。类似于若干绝对值函数相加,不难发现这个函数是单峰的,可以在线段树上二分,同时维护大于和小于 \(x\) 的数的个数与权值和,根据 \(f_mid\) 和 \(f_{mid+1}\) 的关系来判断进入哪个儿子。
点击查看代码
#define int long long
int n,a,b,c[100005],suma,cnta,sumb,cntb;
int root;
struct SegmentTree{
struct node{int ls,rs,cnt,sum;}T[3200005];
int tot=0;
void pushup(int o){
T[o].cnt=T[T[o].ls].cnt+T[T[o].rs].cnt;
T[o].sum=T[T[o].ls].sum+T[T[o].rs].sum;
}
void modify(int &o,int l,int r,int x){
if(!o)o=++tot;
if(l==r){T[o].sum+=x,T[o].cnt++;return;}
int mid=l+r>>1;
x<=mid?modify(T[o].ls,l,mid,x):modify(T[o].rs,mid+1,r,x);
pushup(o);
}
int f(int x){return a*(cnta*x-suma)+b*(sumb-cntb*x);};
void query(int o,int l,int r){
if(l==r){
suma+=T[o].sum,cnta+=T[o].cnt;
std::cout<<f(l)<<'\n';
return;
}
int mid=l+r>>1;
suma+=T[T[o].ls].sum,cnta+=T[T[o].ls].cnt;
sumb+=T[T[o].rs].sum,cntb+=T[T[o].rs].cnt;
if(f(mid)<f(mid+1)){
suma-=T[T[o].ls].sum,cnta-=T[T[o].ls].cnt;
query(T[o].ls,l,mid);
}
else{
sumb-=T[T[o].rs].sum,cntb-=T[T[o].rs].cnt;
query(T[o].rs,mid+1,r);
}
}
}t;
main(){
std::cin.tie(nullptr)->sync_with_stdio(false);
time_st=clock();
freopen("c.in","r",stdin);
freopen("c.out","w",stdout);
std::cin>>n>>a>>b;
for(int i=1;i<=n;++i)std::cin>>c[i];
int mx=*std::max_element(c+1,c+1+n);
for(int i=1;i<=n;++i){
t.modify(root,0,mx,c[i]);
suma=cnta=sumb=cntb=0;
t.query(root,0,mx);
}
time_ed=clock();
std::cerr<<"Time:"<<((time_ed-time_st)/CLOCKS_PER_SEC)<<"s"<<std::endl;
return 0;
}
金牌
题意简述:一棵树,长度为 \(d\) 的路径权值为 \(2^d\),\(q\) 次询问 \(x,y\),求所有通过顶点 \(x\) 和 \(y\) 的简单路径权值之和。
先预处理求出每一个 \(i\) 子树内所有 \(2^d\) 的权值和 \(f_i\),先钦定 \(dep_x < dep_y\),分情况讨论,若 \(x\) 不是 \(y\) 的祖先,\(x,y\) 都能取遍子树节点,答案为 $f_x\times f_y\times dis(x,y) $。若 \(x\) 是 \(y\) 的祖先,\(y\) 可选子树节点,\(x\) 可以选整棵树除掉 \(x\) 在 \(y\) 方向上的儿子 \(z\) 的子树。可以换根 dp 预处理出以 \(i\) 为根的权值 \(g_i\)。答案就为 \(f_y\times (g_x-f_z)\times dis(x,y)\)。树剖求 lca 的话时间复杂度为 \(O(n\log n)\),轻微卡常。
点击查看代码
fastio::IN fin;
fastio::OUT fout;
const int N=1000000;
int n,q,base[N+5],dep[N+5],siz[N+5],fa[N+5],son[N+5],top[N+5],id[N+5],val1[N+5],val2[N+5],tot;
struct node{int to,next;}g[N<<1|1];int head[N+5],o;
inline void add(int x,int y){g[++o]={y,head[x]};head[x]=o;}
void dfs1(int x,int f){
dep[x]=dep[f]+1,siz[x]=1,fa[x]=f;
for(int i=head[x],y;i;i=g[i].next){
if((y=g[i].to)^f){
dfs1(y,x);
siz[x]+=siz[y];
if(siz[y]>siz[son[x]])son[x]=y;
}
}
}
void dfs2(int x,int t){
top[x]=t,id[x]=++tot;
if(!son[x])return;
dfs2(son[x],t);
for(int i=head[x],y;i;i=g[i].next){
if((y=g[i].to)^fa[x]&&y^son[x])dfs2(y,y);
}
}
void dfs3(int x,int f){
val1[x]=1;
for(int i=head[x],y;i;i=g[i].next){
if((y=g[i].to)^f){
dfs3(y,x);
Modadd(val1[x],Mul(2,val1[y]));
}
}
}
void dfs4(int x,int f){
for(int i=head[x],y;i;i=g[i].next){
if((y=g[i].to)^f){
val2[y]=Add(val1[y],Mul(2,Dec(val2[x],Mul(2,val1[y]))));
dfs4(y,x);
}
}
}
inline int lca(int x,int y){
for(;top[x]^top[y];x=fa[top[x]])if(dep[top[x]]<dep[top[y]])std::swap(x,y);
return dep[x]<dep[y]?x:y;
}
inline int getdist(int x,int y){return dep[x]+dep[y]-2*dep[lca(x,y)];}
inline int find(int x,int y){
while(dep[x]>dep[y]){
if(fa[top[x]]==y)return top[x];
x=fa[top[x]];
}
return son[y];
}
int main(){
freopen("d.in","r",stdin);
freopen("d.out","w",stdout);
fin>>n;
for(int i=0;i<=n;++i)base[i]=!i?1:Mul(base[i-1],2);
for(int i=1,u,v;i<n;++i)fin>>u>>v,add(u,v),add(v,u);
dfs1(1,0);dfs2(1,1);dfs3(1,0);val2[1]=val1[1];dfs4(1,0);
fin>>q;
for(int i=1,x,y;i<=q;++i){
fin>>x>>y;
if(dep[x]>dep[y])std::swap(x,y);
int z=lca(x,y),w=getdist(x,y),ans;
if(z!=x){
ans=Mul(Mul(val1[x],val1[y]),base[w]);
fout<<ans<<'\n';
continue;
}
int to=find(y,x);
ans=Mul(Mul(val1[y],Dec(val2[x],Mul(2,val1[to]))),base[w]);
fout<<ans<<'\n';
}
return 0;
}
大眼鸹猫
发现始终满足单调不降的条件,这个限制没有用。把每一位拆开考虑,需要修改总量为 \(|a_i-b_i|\),修改次数一定时,我们希望 \(|a_i-b_i|\) 尽可能平均到每一次,而且修改次数越多肯定答案越小。初始我们把修改次数都设为 1,可以用一个堆来维护修改次数增大 1 时答案的减小量的最小值。
点击查看代码
#include<bits/stdc++.h>
#define mp std::make_pair
#define fi first
#define se second
using ll=long long;
using pii=std::pair<ll,int>;
const int P=998244353;
int n,m;
ll a[100005],b[100005],c[100005],d[100005],ans;
std::priority_queue<pii,std::vector<pii>> pq;
ll calc(ll tot,ll step){return (step-tot%step)*(tot/step)*(tot/step)+(tot%step)*(tot/step+1)*(tot/step+1);}
int main(){
std::cin.tie(nullptr)->sync_with_stdio(false);
freopen("attend.in","r",stdin);
freopen("attend.out","w",stdout);
std::cin>>n>>m;
for(int i=1;i<=n;++i)std::cin>>a[i];
for(int i=1;i<=n;++i)std::cin>>b[i];
for(int i=1;i<=n;++i){
if(a[i]==b[i])continue;
c[i]=std::abs(a[i]-b[i]);
ans+=c[i]*c[i]%P;
d[i]=1;
m--;
if(d[i]<c[i]){
pq.push(mp(calc(c[i],d[i])-calc(c[i],d[i]+1),i));
}
}
if(!m)std::cout<<0,exit(0);
if(m<0)std::cout<<-1,exit(0);
ans%=P;
while(m--){
if(pq.empty())break;
auto now=pq.top();
pq.pop();
ans-=now.fi;
if(d[now.se]<c[now.se]){
d[now.se]++;
pq.push(mp(calc(c[now.se],d[now.se])-calc(c[now.se],d[now.se]+1),now.se));
}
}
std::cout<<(ans%P+P)%P;
return 0;
}
小猫吃火龙果
只有两种颜色的部分分,如果有 A 最后答案一定是 A,线段树维护区间翻转,区间查询个数即可。
考虑没有修改时,可以直接分块预处理出每种颜色最后到的结果。加上修改其实就是六种映射关系的置换,分块维护标记即可。散块重构还会带上 \(3\times 6\) 的常数,所以实际块长要取小一些。
点击查看代码
namespace Subtask3{
static int L[1500],R[1500],tag[1500],bel[200005],f[6][1500][3],bsiz,bnum;
static const int id[6][3]={{0,1,2},{0,2,1},{1,0,2},{1,2,0},{2,0,1},{2,1,0}};
static const int map[3][6]={{2,3,0,1,5,4},{5,4,3,2,1,0},{1,0,4,5,2,3}};
void build(){
bsiz=160;bnum=n/bsiz;if(!bnum)bnum=1;
for(int i=1;i<=bnum;++i)L[i]=R[i-1]+1,R[i]=bsiz*i;R[bnum]=n;
for(int i=1;i<=bnum;++i)for(int j=L[i];j<=R[i];++j)bel[j]=i;
}
void update(int o){
if(tag[o]){
for(int i=L[o];i<=R[o];++i)s[i]=id[tag[o]][s[i]-'A']+'A';
tag[o]=0;
}
}
void rebuild(int o){
for(int p=0;p<6;++p){
for(int c=0;c<3;++c){
int now=c;
for(int j=L[o];j<=R[o];++j)now=w[now][id[p][s[j]-'A']]==1?now:id[p][s[j]-'A'];
f[p][o][c]=now;
}
}
}
void modify(int l,int r,char x,char y){
if(bel[l]==bel[r]){
update(bel[l]);
for(int i=l;i<=r;++i){
if(s[i]==x)s[i]=y;
else if(s[i]==y)s[i]=x;
}
rebuild(bel[l]);
return;
}
update(bel[l]);
for(int i=l;i<=R[bel[l]];++i){
if(s[i]==x)s[i]=y;
else if(s[i]==y)s[i]=x;
}
rebuild(bel[l]);
int p=x-'A'+y-'A'-1;
for(int i=bel[l]+1;i<bel[r];++i)tag[i]=map[p][tag[i]];
update(bel[r]);
for(int i=L[bel[r]];i<=r;++i){
if(s[i]==x)s[i]=y;
else if(s[i]==y)s[i]=x;
}
rebuild(bel[r]);
}
char query(int l,int r,int c){
if(bel[l]==bel[r]){
if(tag[bel[l]])update(bel[l]),rebuild(bel[l]);
int now=c;
for(int i=l;i<=r;++i)now=w[now][s[i]-'A']==1?now:s[i]-'A';
return now+'A';
}
int now=c;
if(tag[bel[l]])update(bel[l]),rebuild(bel[l]);
for(int i=l;i<=R[bel[l]];++i)now=w[now][s[i]-'A']==1?now:s[i]-'A';
for(int i=bel[l]+1;i<bel[r];++i)now=f[tag[i]][i][now];
if(tag[bel[r]])update(bel[r]),rebuild(bel[r]);
for(int i=L[bel[r]];i<=r;++i)now=w[now][s[i]-'A']==1?now:s[i]-'A';
return now+'A';
}
void main(){
build();
for(int i=1;i<=bnum;++i){
for(int p=0;p<6;++p){
for(int c=0;c<3;++c){
int now=c;
for(int j=L[i];j<=R[i];++j){
now=w[now][id[p][s[j]-'A']]==1?now:id[p][s[j]-'A'];
}
f[p][i][c]=now;
}
}
}
for(int i=1;i<=m;++i){
if(q[i].opt==0){
if(q[i].x!=q[i].y)
modify(q[i].l,q[i].r,q[i].x,q[i].y);
}
else{
std::cout<<query(q[i].l,q[i].r,q[i].x-'A')<<'\n';
}
}
}
}