【OI】思训赛日志

01 测试日期:2025.12.5

T1 CF911E Stack Sorting

分析

用栈来模拟,能 \(pop\)\(pop\),记下一个需要 \(pop\) 的数为 \(temp\) ,那么如果栈非空,栈顶肯定大于 \(temp\)
,那么加入栈栈顶值 \(-1\)\(temp\) 的值,否则加入栈 \(n\)\(temp\) 的值,如果需要加入的数之前已经出现
过,答案则不存在。

代码

#include <bits/stdc++.h>
#define mp make_pair 
#define rep(i,a,b) for(int i=(a);i<=(b);i++) 
#define per(i,a,b) for(int i=(a);i>=(b);i--) 
using namespace std;

const int N=2e5+10;

int n,k,t,x[N],ans[N],p[N];
stack <pair<int,int> > s;

int main(){
	cin>>n>>k;
	rep(i,1,k){
		cin>>p[i];
		x[p[i]]=n-i+1;
	}
	per(i,n,1){
		if(!x[i]) s.push(mp(i,x[i]));
		else if(x[i]==t+1) ans[++t]=i;
		else if(x[i]<t+1){
			cout<<"-1\n";
			return 0;
		}
		else{
			while(!s.empty()){
				if(x[i]>t+1){
					if(s.top().second==t+1||s.top().second==0){
						ans[++t]=s.top().first;
						s.pop();	
					}
					else break;
				}
				else{
					ans[++t]=i;
					break;
				}
			}
			if(x[i]!=t) s.push(mp(i,x[i]));
		}
	}
	while(!s.empty()){
		if(s.top().second==t+1||s.top().second==0) ans[++t]=s.top().first,s.pop();
		else{
			cout<<"-1\n";
			return 0;
		}
	}
	per(i,n,1) cout<<ans[i]<<" ";
	return 0;
}

T2 CF540E Infinite Inversions

分析

使用树状数组求逆序对,现在面临的问题是值域过大。我们发现,除了 \(\le 2n\) 个有交换操作的点之外,其余的点是没有动过的,于是可以把一段未修改的区间缩成一个点。再一次巨大离散化,就转化为树状数组求逆序对的模板了。时间复杂度 \(O(n \log n)\)。注意要开 long long!

代码

#include <bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
#define mid (l+r)/2
#define int long long
using namespace std;
const int N=4e5+10;

int n,m,Q;
int a[N],b[N],c[N],num[N],pos[N],f[N],g[N];
map <int,int> mp;

int lowbit(int x){
	return x&(-x);
}

void add(int x,int num){
	while(x){
		c[x]+=num;
		x-=lowbit(x);
	}
}

int query(int x){
	int res=0;
	while(x<N){
		res+=c[x];
		x+=lowbit(x);
	}
	return res;
} 

int val(int x){
	return lower_bound(num+1,num+n+1,x)-num;
}

signed main(){
	cin>>Q;
	rep(i,1,Q){
		cin>>a[i]>>b[i];
		num[++n]=a[i],num[++n]=b[i];
	}
	sort(num+1,num+n+1);
	n=unique(num+1,num+n+1)-num-1;
	int nn=n;
	rep(i,2,nn){
		if(num[i]-num[i-1]>1) {
			num[++n]=num[i-1]+1;
			mp[num[i-1]+1]=num[i]-1-num[i-1];
		}
	} 
	sort(num+1,num+n+1);
	rep(i,1,n){
		f[i]=i;
		if(mp.find(num[i])!=mp.end()) g[i]=mp[num[i]];
		else g[i]=1;
	}
	int ans=0;
	rep(i,1,Q) swap(f[val(a[i])],f[val(b[i])]),swap(g[val(a[i])],g[val(b[i])]);
	rep(i,1,n){
		ans+=query(f[i])*g[i];
		add(f[i],g[i]);
	}
	cout<<ans;
	return 0;
}

T3 CF1453E Dog Snacks

分析

二分答案+树形dp。

因为每次都要去任意一个距离最近的节点,所以容易贪心地想到,对于一个节点 \(x\),肯定是一棵一棵子树地走最优。我们设 \(f_i\) 表示以 \(i\) 为根的子树走完后还剩下多少步,这里的“剩下的步数”指的是走完回到根节点后还能够继续走的距离,如果以 \(i\) 为根的子树无法合法地走完,那么令 \(f_i=−1\)

显然,对于节点 \(x\),只要他的任意儿子 \(y\) 满足 \(f_y ≥2\),那么以 \(x\) 为根的子树就一定可以走完,如果任意一个儿子 \(y\) 满足 \(f_y ≤1\),那就无法走完,但特殊地,如果节点 \(1\) 有至多一个儿子 \(y\) 满足 \(f_y=1\),还是可以走完的,因为刚好回到了节点 \(1\) 上。

转移的时候,有:

\[f_u=\max_{v \in G_u} f_v -1 \]

最后在判定的时候只要 \(f_1=−1\) 就不行,否则可以。

代码

#include <bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
#define over {f[u]=-1;return;}
#define mid (l+r)/2
using namespace std;
const int N=2e5+10;
int f[N];
int n,T,m;
vector <int> g[N];

void dfs(int u,int fa,int k){
	if(g[u].size()==1&&u!=1){
		f[u]=k;
		return ;
	}
	int cnt=0,maxx=0;
	rep(i,0,g[u].size()-1){
		int v=g[u][i];
		if(v==fa) continue;
		dfs(v,u,k);
		if(f[v]==-1) over
		if(f[v]<=1){
			if(u==1){
				if(cnt==1) over
				cnt++;
			}
			else over
		}
		maxx=max(maxx,f[v]);
	}
	f[u]=maxx-1;
}

bool check(int k){
	rep(i,1,n) f[i]=-1;
	dfs(1,0,k);
	return (f[1]!=-1);
}

int find(){
	int l=1,r=n,ans=n+2;
	while(l<=r){
		if(check(mid)) ans=min(ans,mid),r=mid-1;
		else l=mid+1;
	}
	return ans;
}

int main(){
	cin>>T;
	while(T--){
		cin>>n;
		rep(i,1,n-1){
			int u,v;
			scanf("%d%d",&u,&v);
			g[u].push_back(v);
			g[v].push_back(u);
		}
		int ans=find();
		printf("%d\n",ans);
		rep(i,1,n) g[i].clear();
	}
	return 0;
} 

T4 CF1334E Divisor Paths

分析

走一条边实际上就是增加或减少一个素因子。于是可以先把结论猜出来:即从 \(u\) 变到 \(v\) ,需经过 \(\gcd(u,v)\) 中转。也就是 \(u \to \gcd(u,v) \to v\) 。走的过程中只要不走回头路就能保证路径最短。

接下来计数即可,考虑 \(u \to \gcd(u,v)\) 的路径,就等价于 \(\frac{u}{\gcd(u,v)}\to 1\) 的路径。可以通过调整每次走哪个素因子来得到不同的最短路走法,问题转化为 \(\frac{u}{\gcd(u,v)}\) 的素因子的可重排列。由对称性, \(\gcd(u,v) \to v\) 的处理同理。最后两边方案乘起来。

实现的时候,一开始就把质因数做预处理,并且只需要分解 \(D\) 的素因数。因为 \(u,v\) 一定是 \(D\) 的因数,所以 \(u,v\) 的素因数一定是 \(D\) 的素因数。可以用 \(O(n^{\frac{1}{3}})\) 大概估计一个数 \(n\) 的因数个数,而素因数个数一定更小,所以 \(D\) 的素因数个数一定远远小于 \(10^5\)

代码

#include <bits/stdc++.h>
#define mp make_pair
#define gcd __gcd 
#define int long long
#define itt int  
#define rep(i,a,b) for(int i=(a);i<=(b);i++) 
#define repp(i,a,b,c) for(int i=(a);i<=(b);i+=(c)) 
#define per(i,a,b) for(int i=(a);i>=(b);i--) 
using namespace std;

const int N=3e5+10,mod=998244353;

itt d,n,q,cnt,cnt1[N],cnt2[N];
int fact[N/100]={1},inv[N/100]={1},prime[N];

int ksm(int a,int b){
	int s=1;
	while(b){
		if(b&1) s=a*s%mod;
		b>>=1,a=a*a%mod;
	}
	return s%mod;
}

signed main(){
	cin>>d>>q;
	rep(i,1,100) fact[i]=fact[i-1]*i%mod;
	inv[100]=ksm(fact[100],mod-2);
	per(i,99,1) inv[i]=inv[i+1]*(i+1)%mod; 
	int tmp=d; 
	rep(i,2,d/i+1){
		if(tmp%i==0) prime[++cnt]=i;
		while(tmp%i==0) tmp/=i;
	}
	if(tmp>1) prime[++cnt]=tmp;
	rep(cas,1,q){
		int u,v,x;
		scanf("%lld%lld",&u,&v);
		x=gcd(u,v);
		u/=x,v/=x;
		int I=0,J=0,U=u,V=v,i=0,j=0;
		rep(y,1,cnt){
			int k=prime[y];
			if(u%k==0) i++;
			while(u%k==0){
				I++; 
				cnt1[i]++;
				u/=k;
			}
		}
		rep(y,1,cnt){
			int k=prime[y];
			if(v%k==0) j++;
			while(v%k==0){
				J++;
				cnt2[j]++;
				v/=k;
			}
		}
		int ans1=fact[I],ans2=fact[J];
		rep(k,1,i) ans1=ans1*inv[cnt1[k]]%mod;
		rep(k,1,j) ans2=ans2*inv[cnt2[k]]%mod;
		printf("%lld\n",ans1*ans2%mod);
		rep(k,1,i) cnt1[k]=0;
		rep(k,1,j) cnt2[k]=0; 
	}
	return 0;
}

02 测试日期:2025.12.12

T1 CF246E Blood Cousins Return

分析

先将字符串离散化成数字,然后就转化为了数颜色问题,这个问题在序列上做是简单的。

接下来想如何把树上问题转化为序列上的的问题。由于问的节点是跟深度有关,所以我们考虑先处理出树的 bfs 序。因为在 bfs 序上,同一深度的点是连续的。所以询问就变成了序列上区间数颜色的问题,用莫队解决就好了。

然后想如何快速的定位区间,我们需要在至多 \(O(\log^2 n)\) 的时间内定位出查询区间,容易想到一种暴力求法,就是在 bfs 的时候记录下每一个深度的起始位置和结束位置,对于需要查询的深度,从起始位置开始,找到第一个属于子树 \(u\)\(v\) ;从结束位置开始,找到最后一个属于子树 \(u\)\(v\)。这样定位显然是会超时的。

怎么优化呢?想一想,我们的询问除了深度的约束,还有子树的约束,而一棵子树中所有的点在 dfs 序下是连续的。于是可以这样做,求一遍 dfs 序,对于同一深度的节点,用同一个容器来维护他们的 dfs 序(比如存在一个 vector 里),同时记录每棵子树第一个 dfs 到的元素的 dfs 序和最后一个的 dfs 序。二分定位即可。单次定位复杂度 \(O(\log n)\)

之后就是莫队了。在 \(n,m\) 同阶的情况下,时间复杂度为 \(O(n \log n + n \sqrt n)\)

代码

#include <bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
#define mid (l+r)/2
using namespace std;
const int N=1e5+10;
int n,Q,root,max_col,max_dep,m,res,timer;
int a[N],col[N],dep[N],sta[N],ed[N],cnt[N],dfn[N],out[N];
struct query{
	int L,R,ans,id,b;
}qu[N];

vector <int> g[N],b[N];
map <string,int> mp;

void dfs(int u,int fa){
	dfn[u]=++timer;
	dep[u]=dep[fa]+1;
	if(b[dep[u]].size()==0) b[dep[u]].push_back(-1);
	b[dep[u]].push_back(dfn[u]);
	for(auto v:g[u]) dfs(v,u);
	out[u]=timer;
	return ;
}

void bfs(int st){
	queue <int> q;
	stack <int> s;
	q.push(st);
	while(!q.empty()){
		int u=q.front();
		max_dep=max(max_dep,dep[u]);
		a[++m]=u;
		if(!sta[dep[u]]) sta[dep[u]]=m;
		if(!ed[dep[u]-1]) ed[dep[u]-1]=m-1;
		for(auto v:g[u]) q.push(v);
		q.pop();
	}
	ed[max_dep]=m;
}

bool cmp1(query A,query B){
	if(A.L!=B.L) return A.L<B.L;
	else return A.b<B.b;
}

bool cmp2(query A,query B){
	return A.id<B.id;
}

void add(int x){
	cnt[a[x]]++;
	if(cnt[a[x]]==1) res++;
}

void del(int x){
	cnt[a[x]]--;
	if(cnt[a[x]]==0) res--;
}

int getL(int z,int u){
	return lower_bound(b[z].begin()+1,b[z].end(),dfn[u])-b[z].begin()-1;
}

int getR(int z,int u){
	return upper_bound(b[z].begin()+1,b[z].end(),out[u])-b[z].begin()-2;
}

int main(){
	scanf("%d",&n);
	int len=sqrt(n);
	rep(i,1,n){
		string s;
		int fa;
		cin>>s;
		scanf("%d",&fa);
		if(mp.find(s)==mp.end()) col[i]=++max_col,mp[s]=max_col;
		else col[i]=mp[s];
		g[fa].push_back(i);
	}
	col[0]=++max_col;	
	dfs(root,n+1);bfs(root);
	scanf("%d",&Q);
	rep(i,1,Q){
		int u,k,z;
		scanf("%d%d",&u,&k);
		qu[i].id=i,z=dep[u]+k;
		qu[i].L=sta[z]+getL(z,u);
		qu[i].R=sta[z]+getR(z,u);
		qu[i].b=(qu[i].R-1+len)/len;
	}
	sort(qu+1,qu+Q+1,cmp1);
	int l=0,r=0;
	rep(i,1,n+1)a[i]=col[a[i]];
	rep(i,1,Q){
		while(r<qu[i].R) add(++r);
		while(l>qu[i].L) add(--l);
		while(l<qu[i].L) del(l++);
		while(r>qu[i].R) del(r--);
		qu[i].ans=res;
	}
	sort(qu+1,qu+Q+1,cmp2);
	rep(i,1,Q) printf("%d\n",qu[i].ans);
	return 0;
} 

T2 CF1375E Inversion SwapSort

分析

经过一定的思考我们能够发现,本题的过程类似于冒泡排序的过程,最后的目的是将其变成一个单调不减数列。

那么,我们如何使用冒泡排序将其解决呢?

我们发现,冒泡排序的每一次操作是交换两个相邻位置的值,很明显本题可以交换的数有可能是非相邻的,所以我们需要对原数列进行一定的操作。

如何操作才能使:

交换两个位置上的数 \(\longleftrightarrow\) 交换相邻的两个值

我们发现,一个排列的逆排列是可以做到的。

因为对于一个长度为 \(n\) 排列 \(a[i]\)(满足 \(a[i] \in [1,n]\) 且没有重复元素),他的逆排列的定义是 \(b[a[i]] = i\) ,深入解读他的意思其实是 \(a[i]\) 在原排列中处于的位置,且我们发现若两个元素组成一组逆序对,在他的逆排列中他们值的大小依然相反,所以我们发现:

原问题 \(\longleftrightarrow\) 通过冒泡排序将 \(b[i]\) 排序为 \(1,2,3,4,\cdots,n\)

至于交换路径我们只需要把我们交换的值记录下来就可以了。

代码

#include <bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
using namespace std;
const int N=1e6+10;

struct node{
	int id,w;	
}a[N];
int n,x;
int b[N],ansu[N],ansv[N];

bool cmp(node a,node b){
	if(a.w!=b.w) return a.w<b.w;
	else return a.id<b.id;
}

int main(){
	scanf("%d",&n);
	rep(i,1,n) scanf("%d",&a[i].w),a[i].id=i;
	sort(a+1,a+n+1,cmp);
	rep(i,1,n) b[i]=a[i].id;
	rep(i,1,n){
		rep(j,1,n-i){
			if(b[j]>b[j+1]){
				swap(b[j],b[j+1]);
				x++,ansu[x]=b[j],ansv[x]=b[j+1];
			}
		}
	}
	cout<<x<<"\n";
	rep(i,1,x) printf("%d %d\n",ansu[i],ansv[i]);
	return 0;
} 

T3 CF847E Packmen

分析

二分答案。

check 的时候,用到一些贪心的想法。

维护两个变量 \(pos\)\(lst\),分别表示上一个还没吃掉的物品的位置和目前能走到的最远的点。设现在 check 的答案为 \(x\),也就是需要在 \(\le x\) 的时间内吃掉所有物品。

遍历各个位置:

对于物品而言,如果它的位置不在最远走到之内,也就是 \(i>lst\),意为它从未到达过,如果此时有 \(pos=-1\) ,则将 \(pos\) 更新为 \(i\)。否则不管。

对于而言,记 \(dis=i-pos\),表示该人距离上一个还没吃掉的物品的位置的距离,分三种情况:

  • \(dis>x\) 人根本跑不到物品的位置,直接判否。
  • \(dis*3<x\) 先向左边跑吃掉物品,再向右边跑的最远的地方,更新 \(lst=i+x-2*dis\)
  • \(dis*3 \ge x\) 先向右边跑到 \(i+ \frac{x-dis}{2}\) ,再跑到左边吃掉物品。
  • 物品被吃掉后,\(pos=-1\)

直到判完最后一格,如果 \(pos=-1\) 代表所有物品吃完,才代表 \(x\) 可行。

代码

#include <bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
#define mid (l+r)/2
using namespace std;
const int N=1e5+10;

int n;
string s;

bool check(int x){
	int pos=-1,lst=-1;
	rep(i,1,n){
		if(i>lst) if(s[i]=='*'&&pos==-1) pos=i;
		if(s[i]=='P'){
			if(pos==-1) lst=i+x;
			else{
				int dis=i-pos;
				if(dis>x) return 0;
				if(dis*3<x) lst=i+x-2*dis;
				else lst=(x-dis)/2+i;
			}
			pos=-1;
		}
	}
	return (pos==-1);
}

int find(){
	int l=1,r=2*n,ans=r+1;s='?'+s;
	while(l<=r){
		if(check(mid)) ans=min(ans,mid),r=mid-1;
		else l=mid+1;
	}
	return ans;
}

signed main(){
	cin>>n>>s;
	cout<<find();
	return 0;
} 

T4 CF1167E Range Deleting

分析

可行的序列其实也就是不存在逆序对的。

一个重要的性质是:如果 \(f(l,r)\) 可行,则 \(r\) 换成 \(r+1 \sim x\) 都可行。因为 \(r\) 变大只会不断删元素。

预处理出四个数组:

  • \(l[i]\)\(i\) 第一次出现的位置。
  • \(r[i]\)\(i\) 最后一次出现的位置。
  • \(ll[i]\)\(i\sim x\) 中元素第一次出现的位置。
  • \(rr[i]\)\(1\sim i\) 中元素最后一次出现的位置。

接下来我们开挖掘机:

找出最大的 \(pl\) 满足 \([1,pl)\) 中不存在逆序对,就是不断地让大元素的 \(l\) 大于小元素的 \(r\)

找出最小的 \(pr\) 满足 \((pr,x]\) 中不存在逆序对,就是不断地让小元素的 \(r\) 小于小元素的 \(l\)

枚举一个指针 \(i \in [1,pl]\) ,右移右端点指针 \(j\),继续开挖掘机,找到最小的 \(j\)\(j\sim x\) 都是可行的答案,统计进去就行。

时间复杂度 \(O(n)\)

代码

#include <bits/stdc++.h>
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
#define mp make_pair 
using namespace std;
const int N=1e6+10;

int a[N],l[N],r[N],ll[N],rr[N];
int n,x;
long long ans;

int main(){
	scanf("%d%d",&n,&x);
	memset(l,0x3f,sizeof(l));
	memset(r,-1,sizeof(r));
	memset(ll,0x3f,sizeof(ll));
	memset(rr,-1,sizeof(rr)); 
	rep(i,1,n){
		scanf("%d",&a[i]);
		l[a[i]]=min(l[a[i]],i);
		r[a[i]]=max(r[a[i]],i);
	}
	rep(i,1,x) rr[i]=max(rr[i-1],r[i]);
	per(i,x,1) ll[i]=min(ll[i+1],l[i]);
	int pl=1,pr=x;
	while(pl<=x&&rr[pl-1]<=l[pl]) pl++;
	while(pr>=1&&ll[pr+1]>=r[pr]) pr--;
	int j=pr;
	rep(i,1,pl){
		while(j<=x&&(j<i||rr[i-1]>=ll[j+1])) j++;
		ans+=x-j+1;
	}
	cout<<ans;
	return 0;
} 

03 测试日期:2025.12.19

T1 CF1398E Two Types of Spells

分析

加倍的法术肯定加给伤害大的。考虑现在有 \(k\) 个加倍的,最好的想法当然是加给现有的前 \(k\) 大。但是要考虑边界情况:也就是如果第一个加倍法术囊括在这前 \(k\) 大里,它前面没有法术使他加倍,这样就不可行。于是我们考虑把最小的那一个加倍法术放在第一个,然后把 \(k\) 个加倍法术的加倍机会给去除了最小的那个加倍法术中其他法术的前 \(k\) 大。

也就是我们需要实现的功能有:

  • 快速修改(加入/删除)
  • 能够查询前 \(k\)
  • 能够查询最小值

于是,我们可以开两棵权值线段树,一颗存所有法术,一颗存加倍法术。还需要配合离散化缩小值域。

代码

#include <bits/stdc++.h>
#define mid (l+r)/2
#define int long long
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
using namespace std;
const int N=2e5+10;

int n,num,cnt;
int op[N],d[N],nm[N];

struct Segment_Tree{
	int a[N*4],sum[N*4];
	void update(int id,int l,int r,int val,int pos){
		if(!(l^r)){
			a[id]+=val;
			sum[id]+=val*nm[l];
			return;
		}
		if(pos<=mid) update(id*2,l,mid,val,pos);
		else update(id*2+1,mid+1,r,val,pos);
		a[id]=a[id*2]+a[id*2+1];
		sum[id]=sum[id*2]+sum[id*2+1];
	}
	int query(int id,int k,int l,int r){
		if(k<=0) return 0;
		if(a[id]<=k) return sum[id];
		if(!(l^r)) return k*nm[l];
		if(a[id*2+1]>=k) return query(id*2+1,k,mid+1,r); 
		else return sum[id*2+1]+query(id*2,k-a[id*2+1],l,mid);
	}
	int minpos(int id,int l,int r){
		if(!(l^r)) return l;
		if(a[id*2]) return minpos(id*2,l,mid);
		else return minpos(id*2+1,mid+1,r);
	}
}S,T; 

int rk(int x){
	return lower_bound(nm+1,nm+num+1,(x))-nm;
}

signed main(){
	cin>>n;
	rep(i,1,n){
		cin>>op[i]>>d[i];
		nm[i]=abs(d[i]);
	}
	sort(nm+1,nm+n+1);
	num=unique(nm+1,nm+n+1)-nm+1;
	rep(i,1,n){
		bool f=(d[i]<0);
		d[i]=rk(abs(d[i]));
		if(f) d[i]=-d[i];
	}
	rep(i,1,n){
		if(d[i]>0){
			if(!op[i]) S.update(1,1,num,1,d[i]);
			else cnt++,S.update(1,1,num,1,d[i]),T.update(1,1,num,1,d[i]);
		}
		else{
			d[i]=-d[i];
			if(!op[i]) S.update(1,1,num,-1,d[i]);
			else cnt--,S.update(1,1,num,-1,d[i]),T.update(1,1,num,-1,d[i]);
		}
		int ans=S.sum[1];
		if(cnt){
			int mp=T.minpos(1,1,num);
			S.update(1,1,num,-1,mp);
			ans+=S.query(1,cnt,1,num);
			S.update(1,1,num,1,mp);
		}
		printf("%lld\n",ans);	
	}
	return 0;
}

T2 CF525D Arthur and Walls

分析

如何让换的点最少?只需要换掉必须换掉的点就好了!有且仅有只有以下四种情况:

接下来 dfs 即可。

代码

#include <bits/stdc++.h>
#define hxj 0
#define sty 1
#define int long long
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
using namespace std;
const int N=2100;

int n,m;
char c[N][N];
int dx[9]={0,-1,-1,-1,0,0,1,1,1};
int dy[9]={0,-1,0,1,-1,1,-1,0,1};

bool ok(int i,int j){
	if(c[i][j]!='*') return hxj;
	if(c[i-1][j-1]=='.'&&c[i][j-1]=='.'&&c[i-1][j]=='.') return sty;
	if(c[i+1][j-1]=='.'&&c[i][j-1]=='.'&&c[i+1][j]=='.') return sty;
	if(c[i-1][j+1]=='.'&&c[i][j+1]=='.'&&c[i-1][j]=='.') return sty;
	if(c[i+1][j+1]=='.'&&c[i][j+1]=='.'&&c[i+1][j]=='.') return sty;
	return hxj;
}

void dfs(int i,int j){
	if(!ok(i,j)) return ;
	c[i][j]='.';
	rep(k,1,8) dfs(i+dx[k],j+dy[k]);
}

signed main(){
	cin>>n>>m;
	rep(i,1,n) rep(j,1,m) cin>>c[i][j]; 
	rep(i,1,n) rep(j,1,m) dfs(i,j);
	rep(i,1,n){
		rep(j,1,m) cout<<c[i][j];
		cout<<"\n";
	}
	return 0;
}

T3 CF1253E Antenna Coverage

分析

转化为图论问题。考虑边 \(i \to i+1\),表示覆盖位置 \(i\),需要 \(1\) 的代价。

再考虑每根天线可以覆盖到区间 \([L,R+1)\),建一条 \(L \to R+1\) 的边权为代价的边,每个天线都如此建边,直到其中一个端点碰到边界,即 \(L=1\)\(R=m\)

注意还要考虑区间重叠,我们还需要建 \(i+1 \to i\) 边权为 \(0\) 的边。

接下来跑最短路即可。边数 \(<O(nm)\),则时间复杂度 \(<O(nm \log m)\),随便跑。

代码

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10;
struct node{
	int b,c;
	bool operator<(const node&AB)const{
		return c>AB.c;
	}
};
priority_queue <node> q;
vector <node> g[N];
int dis[N],a[N],b[N];
int n,m,s,t;
bool vis[N];
void dijkstra(){
	memset(dis,0x3f,sizeof(dis));
	dis[s]=0;
	q.push({s,0});
	while(!q.empty()){
		node tp=q.top();
		int k=tp.b;
		q.pop();
		if(vis[k]) continue;
		vis[k]=1;
		for(auto y:g[k]){
			if(!vis[y.b]&&dis[y.b]>dis[k]+y.c){
				dis[y.b]=dis[k]+y.c;
				q.push({y.b,dis[y.b]});
			}
		}
	}
}

signed main(){
	cin>>n>>m;
	s=1,t=m+1;
	for(int i=1;i<=n;i++) cin>>a[i]>>b[i];
	for(int i=1;i<=m;i++){
		g[i].push_back({i+1,1});
		g[i+1].push_back({i,0});
	}
	for(int i=1;i<=n;i++){
		int l=max(a[i]-b[i],1ll);
		int r=min(a[i]+b[i]+1,m+1);
		int k=0;
		g[l].push_back({r,0});
		while(1){
			l=max(--l,1ll),r=min(++r,m+1);
			g[l].push_back({r,++k});
			if(l==1||r==m+1) break;
		}
	}
	dijkstra();
	cout<<dis[t];
	return 0;
}

T4 CF1225E Rock Is Push

分析

发现该问题类似于过河卒,考虑 dp。由于向右和向下移动所涉及的箱子不同,且如果越过了箱子就一定不会再次推到(不能往回走),考虑如下设置状态:

\(f _{ i,j ​ }\) 为从上方走到 \((i,j)\) 的所有方案数,\(g _{ i,j}\) 为从左方走到 \((i,j)\) 的所有方案数。在不考虑箱子的情况下,显然有:\(f_{i,j}=f_{i-1,j}+g_{i-1,j}\)\(g_{i,j}=g_{i,j-1}+f_{i,j-1}\)

容斥思想下,我们再考虑减去不合法的方案数。我们发现除了第一排和第一列以外,其他行列都可以看作有上一行某处走下来再向右走或从左一列走过来再向下走。因此我们可以抽象出第 \(0\) 行和第 \(0\) 列,并赋予初值 \(f _{ 1,1} =g _{ 1,1} =1\),为防止答案重复统计,同时也赋予初值 \(f_{2,1}=g_{1,2}=1\),容易判断这是正确的。

在此前提下,以除去不合法的向右走方案数为例。我们发现如果往下走到 \((i,j)\),由于 \((i,j)\) 及其左边的箱子都不会被推到,我们只需要判断一直往右走到哪里不合法就可以了。类似于前缀和地定义 \(r _{i,j}\)\((i,j)\) 及其右边(同一行)的箱子数总和。则除去从上方走到 \((i,j)\) 再往右走的不合法方案等价于将 \(g_{ i,m−r_{i,j+1}+1}\) 减去 \(f_{i,j}\),另一个同理。

代码

#include<bits/stdc++.h>
#define int long long
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
using namespace std;
const int N=2e3+10,mod=1e9+7;
int n,m;
char a[N][N];
int f[N][N],g[N][N],r[N][N],d[N][N];
signed main(){
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++) scanf("%s",a[i]+1);
	if(n==1&&m==1){
		printf("1\n");
		return 0;
	}
	per(i,n,1){
		per(j,m,1){
			if(a[i][j]=='R') r[i][j]=d[i][j]=1;
			r[i][j]+=r[i][j+1];d[i][j]+=d[i+1][j];
		}
	}
	f[1][1]=g[1][1]=1,f[2][1]=g[1][2]=-1;
	rep(i,1,n){
		rep(j,1,m){
			f[i][j]=(f[i][j]+f[i-1][j]+g[i-1][j])%mod;
			g[i][m-r[i][j+1]+1]=(g[i][m-r[i][j+1]+1]-f[i][j])%mod;
			g[i][j]=(g[i][j]+g[i][j-1]+f[i][j-1])%mod;
			f[n-d[i+1][j]+1][j]=(f[n-d[i+1][j]+1][j]-g[i][j])%mod;
		}
	}
	printf("%lld\n",(f[n][m]+g[n][m])%mod);
	return 0;
}

04 测试日期:2025.12.26

T1 CF1043E Train Hard, Win Easy

分析

题意就是给定 \(n\) 个人,第 \(i\) 个人有两个数字 \(x_i\)\(y_i\)。每个人可以与除了不能匹配的人之外的人 \(j\) 匹配,匹配后给答案贡献 \(\min(x_i+y_j,x_j+y_i)\) 。问答案。

其实不能匹配的那些可以 \(O(1)\) 减掉,所以考虑所有人都能互相匹配。

贡献的是 \(\min(x_i+y_j,x_j+y_i)\),考虑当 \(x_i+y_j<x_j+y_i\) 的时候取前一项,等价于 \(x_i-y_i<x_j-y_j\)。反过来则取后一项。那么我们可以按照 \(x_i-y_i\) 排序,对于 \(i\),考虑排在其前面的 \(j\),将 \(x_i+y_j\) 加入答案。排在其后面的 \(k\),将 \(x_k+y_i\) 加入答案。使用前缀后缀和辅助维护。

最后减去不能匹配的就行。

代码

#include <bits/stdc++.h>
#define int long long
#define per(i,a,b) for(int i=(a);i>=(b);i--)
#define rep(i,a,b) for(int i=(a);i<=(b);i++) 
using namespace std;
const int N=3e5+10;
struct node{
	int a,b,c,id;
}q[N];
int n,m,pre[N],suf[N],mp[N],ans[N];
vector <int> g[N];

bool cmp(node x,node y){
	return x.b-x.a<y.b-y.a;
}

signed main(){
	cin>>n>>m;
	rep(i,1,n) cin>>q[i].a>>q[i].b,q[i].id=i;
	rep(i,1,m) {
		int u,v;
		cin>>u>>v;
		g[u].push_back(v);
		g[v].push_back(u);
	}
	sort(q+1,q+n+1,cmp);
	rep(i,1,n) pre[i]=pre[i-1]+q[i].b;
	per(i,n,1) suf[i]=suf[i+1]+q[i].a;
	rep(i,1,n) mp[q[i].id]=i;
	rep(i,1,n) ans[q[i].id]=pre[i-1]+suf[i+1]+(i-1)*q[i].a+(n-i)*q[i].b;
	rep(i,1,n) if(g[i].size()>0){ rep(j,0,g[i].size()-1) {int v=g[i][j];ans[i]-=min(q[mp[i]].a+q[mp[v]].b,q[mp[i]].b+q[mp[v]].a);}}
	rep(i,1,n) cout<<ans[i]<<' ';
	return 0;
}

T2 CF1530E Minimax

分析

分类讨论。

1.只有一种字符:直接输出原串

2.只有一种字符出现一次:先输出那一个字符,然后其他的按照字典序排序输出。

3.最小的字符 \(x\) 的出现次数 \(c(x)\le \frac{|s|}{2}\):先构造 \(x\),然后在 \(x\) 的后面按照字典序排放剩下的字母,字母后面再来 \(x\),以此类推。\(x\) 排完之后直接按字典序输出后面的。

4.最小的字符 \(x\) 的出现次数 \(c(x)> \frac{|s|}{2}\) 且串里只有两种字符,\(x\)\(y\)。则构造 \(S=xy\dots yx\dots x\)

5.最小的字符 \(x\) 的出现次数 \(c(x)> \frac{|s|}{2}\) 且串里有大于两种字符,\(x\)\(y\)\(z\) 是前三小。则构造 \(S=xyx\dots xz\),剩下的字典序输出。

代码(未完成)

#include <bits/stdc++.h>
#define int long long
#define bit __builtin_popcount 
#define mp make_pair
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
using namespace std;

const int N=1e5+10;

int n,T,cas;
string s;
int cnt[200]; 

signed main(){
	cin>>T;
	while(T--){
		cin>>s;
		int n=s.size();
		rep(i,0,n-1) cnt[s[i]]++;
		int cls=0;
		rep(i,'a','z'){
			if(cnt[i]&&cnt[i]==n) cas=1;
			if(cnt[i]) cls++;
			if(cnt[i]==1&&cas!=2) cas=2;
		}
		if(cas==1){cout<<s<<'\n';continue;}
		if(cas==2){
			rep(i,'a','z') if(cnt[i]) rep(j,1,cnt[i]) cout<<char(i);
			cout<<'\n';continue;
		}
		rep(i,'a','z'){
			if(cnt[i]&&cnt[i]<=(n+1)/2){
				cas=3;
				break;
			}
			else {
				cas=4;
				break;
			}
		}
		if(cas==4) if(cls>2) cas=5;
		 
	}	
	
	return 0;
} 

T3 CF525E Anya and Cubes

分析

对于一个立方体 \(i\),有三种选择更改答案,\(+0\)\(+a_i\)\(+a_i!\),改为阶乘的操作最多 \(k\) 次,问有多少方案组成 \(S\)

看到 \(n\) 比较小,直接暴力是 \(O(3^n)\),过不了。于是折半搜素,注意阶乘 \(\le 19\) 才有用,其他的可以跳过。最后复杂度为 \(O(k3^\frac{n}{2})\)

代码

#include <bits/stdc++.h>
#define int long long
#define mp make_pair
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
using namespace std;

const int N=30;

int n,k,S,ans,fact[N]={1},a[N]; 

map <pair<int,int>,int> cnt;

void dfs(int l,int r,int sum,int used,int stu){
	if(l==r+1){
		if(!stu) cnt[mp(sum,used)]++;
		else {
			rep(K,0,k) if(cnt.find(mp(S-sum,K-used))!=cnt.end()) ans+=cnt[mp(S-sum,K-used)];
		}
	}
	else if(sum>S) return;
	else{
		dfs(l+1,r,sum,used,stu);
		dfs(l+1,r,sum+a[l],used,stu);
		if(a[l]<=19) dfs(l+1,r,sum+fact[a[l]],used+1,stu);
	}
}

signed main(){
	cin>>n>>k>>S;
	rep(i,1,19) fact[i]=fact[i-1]*i;
	rep(i,1,n) cin>>a[i];
	dfs(1,n/2,0,0,0);
	dfs(n/2+1,n,0,0,1);
	cout<<ans;
	return 0;
}

T4 CF1316E Team Building

分析

看到 \(p\le 7\),可以考虑状压 dp。

首先可以明确观众一定是选能选的里面的前 \(k\) 大,故先把人按 \(a_i\) 降序排序。

\(dp[i][j]\) 为到第 \(i\) 个人时,球队位置选择情况为 \(j\) 的总力量。

如果 \(i \le \operatorname{popcount}(j)+k\),即在 \(j\) 的选位情况下,\(i\) 排在前 \(k\) 大,那么 \(i\) 可以当观众。

\(i\) 不当观众,就可以枚举他去哪一个位置,然后从上一个状态转移过来就行。

代码

#include <bits/stdc++.h>
#define int long long
#define bit __builtin_popcount 
#define mp make_pair
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
using namespace std;

const int N=1e5+10;

int n,k,p,dp[N][150],s[N][10];

struct node{
	int a,id;
}q[N];

bool cmp(node x,node y){
	if(x.a!=y.a) return x.a>y.a;
	else return x.id<y.id;
}

signed main(){
	cin>>n>>p>>k;
	rep(i,1,n) cin>>q[i].a,q[i].id=i;
	rep(i,1,n) rep(j,0,p-1) cin>>s[i][j];
	sort(q+1,q+n+1,cmp);
	memset(dp,-0x3f,sizeof(dp));
	dp[0][0]=0;
	rep(i,1,n){
		rep(msk,0,(1<<p)-1){
			dp[i][msk]=dp[i-1][msk];
			if(i<=bit(msk)+k) dp[i][msk]=max(dp[i][msk],dp[i-1][msk]+q[i].a);
			rep(j,0,p-1) if(msk>>j&1) dp[i][msk]=max(dp[i][msk],dp[i-1][msk^(1<<j)]+s[q[i].id][j]);
		}
	}
	cout<<dp[n][(1<<p)-1];
	return 0;
}

05 测试日期:2026.1.6

T1 CF1244E Minimizing Difference

T2 CF1077E Thematic Contests

T3 CF382C Arithmetic Progression

T4CF1582E Pchelyonok and Segments

posted @ 2025-12-20 22:00  shaotianyu  阅读(35)  评论(0)    收藏  举报