Competition Set - 模拟赛 I

HNOI2017 Day2

2023-06-10
注:Day2T2换为BJOI2017Day2T1,以匹配学习进度


A 大佬

你需要用 \(n\) 天挑战一名大佬。大佬的自信值为 \(C\)。你的目标是在 \(n\) 天内使大佬的自信值恰好为 \(0\)

\(\mathrm{mc}\) 来表示你的自信值上限。在第 \(i \ (i\ge 1)\) 天,大佬会对你发动一次嘲讽,使你的自信值减小 \(a_i\),如果这个时刻你的自信值小于 \(0\) 了,那么你就失败了。否则,你能且仅能选择如下的行为之一:

  1. 还一句嘴,大佬的自信值 \(C\) 减小 \(1\)
  2. 做一天的水题,使得自己的当前自信值增加 \(w_i\),如果超过 \(\mathrm{mc}\) 就变为 \(\mathrm{mc}\)
  3. 让自己的等级值 \(L\)\(1\)
  4. 让自己的讽刺能力 \(F\) 乘以自己当前等级 \(L\),使讽刺能力 \(F\) 更新为 \(F\cdot L\)
  5. 怼大佬,让大佬的自信值 \(C\) 减小 \(F\)。并在怼完大佬之后,你自己的等级 \(L\) 自动降为 \(0\),讽刺能力 \(F\) 降为 \(1\)。这个操作只能做不超过两次。

注意,在任何时候,不能大佬的自信值为负。在第 \(1\) 天被攻击之前,你的自信值等于 \(\mathrm{mc}\),讽刺能力 \(F\)\(1\),等级 \(L\)\(0\)

一共有 \(m\) 个大佬,他们的嘲讽时间都是 \(n\) 天,而且第 \(i\) 天的嘲讽值都是 \(a_i\)。不管和哪个大佬较量,你在第 \(i\) 天做水题的自信回涨都是 \(w_i\)。对每个大佬,问能否战胜。

\(1\le n,\mathrm{mc},a_i,w_i \le 100,1\le m \le 20,1 \le C \le 10^8。\)


tag:DP,枚举

首先,注意到提升自己的自信值与降低大佬的自信值相互独立,所以可以考虑先求出最多能够花多少天来攻击大佬。这只需要做一个简单的DP,状态为天数和自信值。

然后,考虑操作“怼大佬”。预处理出怼大佬所有可能的“伤害”,这样的数不超过 \(10^8\),所有素因子不超过 \(100\),数目并不太多,约为 \(9.2 \times 10^5\)。可以直接dfs。

接下来,求出每个“伤害”所需要的步数,这也可以DP。将一个数拆成若干个数的积,枚举最大值,不断更新拼出某个数的最少因数个数即可。

对于每个询问,先用不怼大佬和怼一次大佬更新答案。怼两次大佬的情况,先枚举第一次怼的伤害,然后在大佬的剩余自信值附近(距离不超过100)枚举第二次伤害,更新最小回合数。比较最小回合数与可用回合数即得答案。

难度Easy+,成功场切。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m,mc,a[105],w[105],ans,dp[105][105],cnt;
int p[30]={0,2,3,5,7,11,13,17,19,23,29,31,37,41,43,47
			,53,59,61,67,71,73,79,83,89,97,101};
vector<int> val;
void dfs(int x,int f,int op){
	if(op)val.push_back(f);
	if(p[x]>n)return;
	dfs(x+1,f,0);
	if(f<1e8/p[x])dfs(x,f*p[x],1);
}
int len,f[1000005],g[1000005];
void init(){
	dfs(1,1,1);
	sort(val.begin(),val.end());
	memset(f,0x3f,sizeof(f));
	memset(g,0x3f,sizeof(g));
	f[0]=g[0]=0;len=val.size();
	for(int i=2;i<=n;++i)
		for(int j=0;j<len;++j)if(val[j]%i==0){
			int p=lower_bound(val.begin(),val.end(),val[j]/i)-val.begin();
			f[j]=min(f[j],f[p]+1);g[j]=min(g[j],f[j]+i);
		}
	for(int i=0;i<len;++i)g[i]=g[i]-val[i]+1;
}
void solve(){
	memset(dp,-0x3f,sizeof(dp));
	dp[0][mc]=0;
	for(int i=1;i<=n;i++)
		for(int j=a[i];j<=mc;j++){
			dp[i][j-a[i]]=max(dp[i][j-a[i]],dp[i-1][j]+1);
			int c=min(j-a[i]+w[i],mc);
			dp[i][c]=max(dp[i][c],dp[i-1][j]);
		}
	for(int i=1;i<=n;i++)
		for(int j=0;j<=mc;j++)
			cnt=max(cnt,dp[i][j]);
}
int main(){
	ios::sync_with_stdio(false);
	cin>>n>>m>>mc;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=n;i++)cin>>w[i];
	init();solve();
	for(int i=1,c;i<=m;i++){
		cin>>c;ans=c;
		for(int j=0;j<len;j++)if(val[j]<=c){
			ans=min(ans,g[j]+c);
			int x=lower_bound(val.begin(),val.end(),c-val[j]-100)-val.begin(),
				y=lower_bound(val.begin(),val.end(),c-val[j]+1)-val.begin();
			for(int k=x;k<y;k++)ans=min(ans,g[j]+g[k]+c);
		}
		printf(ans<=cnt?"1\n":"0\n");
	}
	return 0;
}

B 抛硬币

一枚均匀的硬币,小A抛\(a\)次,小B抛\(b\)次,求小A抛出正面的次数大于小B的情况数。

\(1\le a,b\le 10^{15},b\le a \le b+10000\)


tag:组合恒等式,exLucas

数学题,直接推式子,第三个等号用到了范德蒙恒等式。

\[ans=\sum_{i=1}^a \sum_{j=0}^{i-1} C_a^iC_b^j=\sum_{k=1}^{a} \sum_{i} C_a^{a-i}C_b^{i-k}=\sum_{k=1}^{a}C_{a+b}^{a-k}=2^{a+b-1}-\frac{1}{2} \cdot \sum_{k=b+1}^{a-1} C_{a+b}^{k} \]

用扩展lucas定理求后面的一堆组合数即可。

难度:Medium-,考场exLucas没有实现好,丢了20分。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll a,b,ans,k,c2[1025],c5[1953126],r1,r2,x2,x5,d2,d5,pwr[15][15];string res;
const ll m1=1024,m2=1953125,l1=787109376,l2=212890625,val=976563;
inline void exgcd(ll a,ll b,ll&x,ll&y){
	if(b==0){x=1;y=0;return;}
	exgcd(b,a%b,x,y);ll x0=x,y0=y;
	x=y0;y=x0-a/b*y0;
}
inline ll power(ll a,ll b,ll mod){
	register ll c=1;
	for(;b;b>>=1){
		if(b&1)c=c*a%mod;
		a=a*a%mod;
	}
	return c;
}
inline void f(ll n,ll p,ll k,ll&x,ll&y){
	register ll m,c,cnt=0;
	if(p==2)m=m1,c=c2[m];
	else m=m2,c=c5[m];
	while(n){
		register ll q=n/m,r=n-m*q;cnt+=q;
		x=x*(p==2?c2[r]:c5[r])%m;
		n/=p;y+=n;
	}x=x*power(c,cnt,m)%m;
}
inline ll C(ll n,ll m,ll p,ll k){
	register ll x,y=1,z=1,t,d,e=0,h=0;
	if(p==2)x=x2,d=d2;else x=x5,d=d5;
	f(m,p,k,y,e);f(n-m,p,k,z,h);d-=e+h;
	if(d>=k)return 0;t=pwr[p][k-d];
	register ll r,s,u,v;exgcd(y,t,r,s);exgcd(z,t,u,v);
	r=(r%t+t)%t;u=(u%t+t)%t;
	t=x*r%t*u%t;t=t*pwr[p][d];
	return t;
}
int main(){
	c2[0]=c5[0]=pwr[2][0]=pwr[5][0]=1;
	for(int i=1;i<=10;i++)pwr[2][i]=2*pwr[2][i-1],pwr[5][i]=5*pwr[5][i-1];
	for(register int i=1;i<=1024;++i)
		if(i%2!=0)c2[i]=1ll*c2[i-1]*i%m1;else c2[i]=c2[i-1];
	for(register int i=1;i<=1953125;++i)
		if(i%5!=0)c5[i]=1ll*c5[i-1]*i%m2;else c5[i]=c5[i-1];
	ios::sync_with_stdio(false);
	while(cin>>a>>b>>k){
//		double t1=clock()*1000/CLOCKS_PER_SEC;
		r1=power(2,a+b,m1);r2=power(2,a+b,m2);
		ans=0;res="";x2=x5=1;d2=d5=0;
		f(a+b,2,10,x2,d2);f(a+b,5,9,x5,d5);
		if(a==b){
			r1=(r1-C(a*2,a,2,10)+m1)%m1;
			r2=(r2-C(a*2,a,5,9)+m2)%m2;
		}
		else if(a>b+1){
			ll d=(a+b+1)/2;
			for(register ll m=b+1;m<=d;++m){
				register ll x=C(a+b,m,2,10),y=C(a+b,m,5,9);
				r1=(r1+x)%m1;r2=(r2+y)%m2;
				if(a+b-m>d&&a+b-m<a)r1=(r1+x)%m1,r2=(r2+y)%m2;
			}
		}
		ans=r1/2*l2+r2*val%m2*l1;
		while(k--){res=char(ans%10+'0')+res;ans/=10;}
		cout<<res<<endl;
//		double t2=clock()*1000/CLOCKS_PER_SEC;
//		cout<<(t2-t1)<<"ms\n";
	}
	return 0;
}

C 喷式水战改

一堆玩意儿,每个玩意儿有三个状态0,1,2,每个状态有一个权值。维护这些玩意儿构成的序列,支持在某个位置插入若干个,以及询问:将序列分为4段,每段状态相同,依次为0,1,2,0,总权值和的最大值。

操作数 \(\le 10^5\),权值 \(\le 10^4\),每次插入数目 \(\le 10^9\)


tag:Splay

如果每次只插入一个,那么可以很容易用Splay维护。每个结点维护8种分割的最大值,即目标分割的8个子串。

现在每次插入多个,只要在插入时(如果需要)拆点即可。

难度Medium,考场接近做出,因为Splay刚学印象深。但其实也挺板的?

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=4e5+5;
int n;ll ans,lstans;
int rt,tot,fa[N],ch[N][2];
ll siz[N],cnt[N],val[N][3],sum[N][9];
void init(){rt=1;tot=2;cnt[1]=cnt[2]=1;siz[1]=2;siz[2]=1;fa[2]=1;ch[1][1]=2;}
ll max(ll a,ll b,ll c){return max(max(a,b),c);}
ll max(ll a,ll b,ll c,ll d){return max(max(a,b,c),d);}
void push_up(int p){
	int lc=ch[p][0],rc=ch[p][1];
	siz[p]=siz[lc]+siz[rc]+cnt[p];
	for(int op=0;op<3;op++)
		sum[p][op]=sum[lc][op]+val[p][op]*cnt[p]+sum[rc][op];
	for(int op=3;op<6;op++)
		sum[p][op]=max(sum[lc][op]+val[p][(op+1)%3]*cnt[p]+sum[rc][(op+1)%3],
					   sum[lc][op%3]+val[p][op%3]*cnt[p]+sum[rc][op]);
	sum[p][6]=max(sum[lc][6]+val[p][2]*cnt[p]+sum[rc][2],
				  sum[lc][3]+val[p][1]*cnt[p]+sum[rc][4],
				  sum[lc][0]+val[p][0]*cnt[p]+sum[rc][6]);
	sum[p][7]=max(sum[lc][7]+val[p][0]*cnt[p]+sum[rc][0],
				  sum[lc][4]+val[p][2]*cnt[p]+sum[rc][5],
				  sum[lc][1]+val[p][1]*cnt[p]+sum[rc][7]);
	sum[p][8]=max(sum[lc][8]+val[p][0]*cnt[p]+sum[rc][0],
				  sum[lc][6]+val[p][2]*cnt[p]+sum[rc][5],
				  sum[lc][3]+val[p][1]*cnt[p]+sum[rc][7],
				  sum[lc][0]+val[p][0]*cnt[p]+sum[rc][8]);
}
bool get(int p){return p==ch[fa[p]][1];}
void rotate(int x){
	int y=fa[x],z=fa[y],op=get(x)^1;
	ch[y][op^1]=ch[x][op];if(ch[x][op])fa[ch[x][op]]=y;
	ch[x][op]=y;fa[y]=x;fa[x]=z;if(z)ch[z][y==ch[z][1]]=x;
	push_up(y);push_up(x);
}
void splay(int x,int goal=0){
	for(int p=fa[x];p!=goal;p=fa[x]){
		if(fa[p]!=goal)rotate(get(p)==get(x)?p:x);
		rotate(x);
	}if(!goal)rt=x;
}
int kth(ll&k){
	int p=rt;
	while(1){
		if(siz[ch[p][0]]>=k)p=ch[p][0];
		else{k-=siz[ch[p][0]]+cnt[p];if(k<=0)return p;p=ch[p][1];}
	}
}
void ins(ll x,ll a,ll b,ll c,ll m){
	ll k=x+1;int p=kth(k),q;
	if(k==0){
		k=x+2;q=kth(k);splay(p);splay(q,p);
		ch[q][0]=++tot;fa[tot]=q;cnt[tot]=m;
		val[tot][0]=a;val[tot][1]=b;val[tot][2]=c;
		push_up(tot);push_up(q);push_up(p);
	}
	else{
		ll l=x+1-k-cnt[p];q=kth(l);splay(q);splay(p,q);
		int y=ch[p][0]=++tot;fa[y]=p;cnt[y]=m;
		val[y][0]=a;val[y][1]=b;val[y][2]=c;
		ch[y][0]=++tot;fa[tot]=y;cnt[tot]=k+cnt[p];cnt[p]=-k;
		val[tot][0]=val[p][0];val[tot][1]=val[p][1];val[tot][2]=val[p][2];
		push_up(tot);push_up(y);push_up(p);push_up(q);
	}
}
int main(){
	scanf("%d",&n);
	init();
	for(int i=1;i<=n;i++){
		ll p,a,b,c,x;
		scanf("%lld%lld%lld%lld%lld",&p,&a,&b,&c,&x);
		ins(p,a,b,c,x);
		splay(1);splay(2,1);ans=sum[ch[2][0]][8];
		printf("%lld\n",ans-lstans);
		lstans=ans;
	}
	return 0;
}

HNOI2017 Day1

2023-06-09


A 单旋

考虑一棵单旋实现的splay,支持以下操作:插入;单旋最小值;单旋最大值;单旋并删除最小值;单旋并删除最大值。其中单旋表示将点转到根。每次操作的代价是操作的点的深度(插入后或单旋前)。\(m\)次操作,求每次操作的代价。

\(m \le 10^5\),所有结点的关键码互不相同。


tag:Splay

事实上这个题肯定不能用Splay做,但分析这样一棵Splay的性质还是很关键的。

模拟几个操作可以发现,单旋最值其实只是把最值换到根,然后最值的儿子(唯一)代替它原来的位置。那么它对深度的影响就非常明确:对于最小值\(u\),设它的父亲权值是\(v\),那么单旋使\(u\)的深度变为\(1\)\(v,...,n\)的深度+1;删除使\(u,...,v-1\)的深度-1。

我们可以把所有点权离散化,然后在线段树上修改。对于不在树上的点,可以一并修改,只要插入的时候用赋值方式即可。

至于插入操作,容易发现插入一个点,要么成为它前驱的右儿子,要么成为它后继的左儿子,且应选取二者中深度较大的一个。用一个set维护在树上的点权即可。

难度Medium,难点在于插入。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,q[N][2],x[N],rt,fa[N],ch[N][2];
struct SegmentTree{
	int tag[N<<2];
	#define mid ((l+r)>>1)
	void modify(int p,int l,int r,int L,int R,int v){
		if(l>=L&&r<=R){tag[p]+=v;return;}
		if(L<=mid)modify(p<<1,l,mid,L,R,v);
		if(R>mid)modify(p<<1|1,mid+1,r,L,R,v);
	}
	int query(int p,int l,int r,int x){
		if(l==r)return tag[p];
		if(x<=mid)return query(p<<1,l,mid,x)+tag[p];
		else return query(p<<1|1,mid+1,r,x)+tag[p];
	}
	#undef mid
}seg;
set<int> S;
int main(){
	scanf("%d",&m);
	for(int i=1;i<=m;i++){
		scanf("%d",&q[i][0]);
		if(q[i][0]==1){
			scanf("%d",&q[i][1]);
			x[++n]=q[i][1];
		}
	}
	sort(x+1,x+n+1);
	for(int i=1;i<=m;i++){
		int op=q[i][0];
		if(op==1){
			int v=lower_bound(x+1,x+n+1,q[i][1])-x,res;
			if(S.empty()){rt=v;res=1;}
			else{
				auto it=S.lower_bound(v);
				if(it==S.end()){
					int u=*--it;ch[u][1]=v;fa[v]=u;
					res=seg.query(1,1,n,u)+1;
				}
				else if(it==S.begin()){
					int u=*it;ch[u][0]=v;fa[v]=u;
					res=seg.query(1,1,n,u)+1;
				}
				else{
					int u1=*it,u2=*--it;
					int du1=seg.query(1,1,n,u1),
						du2=seg.query(1,1,n,u2);
					if(du1>du2)ch[u1][0]=v,fa[v]=u1,res=du1+1;
					else ch[u2][1]=v,fa[v]=u2,res=du2+1;
				}
			}
			S.insert(v);
			int dv=seg.query(1,1,n,v);
			seg.modify(1,1,n,v,v,res-dv);
			printf("%d\n",res);
		}
		if(op==2){
			int v=*S.begin(),res;
			printf("%d\n",res=seg.query(1,1,n,v));
			seg.modify(1,1,n,v,v,1-res);
			if(fa[v]){
				seg.modify(1,1,n,fa[v],n,1);
				ch[fa[v]][0]=ch[v][1];if(ch[v][1])fa[ch[v][1]]=fa[v];
				ch[v][1]=rt;fa[rt]=v;fa[v]=0;rt=v;
			}
		}
		if(op==3){
			int v=*--S.end(),res;
			printf("%d\n",res=seg.query(1,1,n,v));
			seg.modify(1,1,n,v,v,1-res);
			if(fa[v]){
				seg.modify(1,1,n,1,fa[v],1);
				ch[fa[v]][1]=ch[v][0];if(ch[v][0])fa[ch[v][0]]=fa[v];
				ch[v][0]=rt;fa[rt]=v;fa[v]=0;rt=v;
			}
		}
		if(op==4){
			int v=*S.begin(),res;
			printf("%d\n",res=seg.query(1,1,n,v));
			seg.modify(1,1,n,v,!fa[v]?n:fa[v]-1,-1);
			if(fa[v]){ch[fa[v]][0]=ch[v][1];if(ch[v][1])fa[ch[v][1]]=fa[v];}
			else{rt=ch[v][1];ch[v][1]=fa[rt]=0;}
			S.erase(S.begin());
		}
		if(op==5){
			int v=*--S.end(),res;
			printf("%d\n",res=seg.query(1,1,n,v));
			seg.modify(1,1,n,fa[v]+1,v,-1);
			if(fa[v]){ch[fa[v]][1]=ch[v][0];if(ch[v][0])fa[ch[v][0]]=fa[v];}
			else{rt=ch[v][0];ch[v][0]=fa[rt]=0;}
			S.erase(--S.end());
		}
	}
	return 0;
}

B 影魔

给定\(1,2,...,n\)的排列\(a\),对一个区间\([l,r](l<r)\),定义\(m=\max_{i=l+1}^{r-1}a_i\),其贡献为:

  • \(m\)不存在或\(m\lt a_l\)\(m \lt a_r\),则贡献为\(p1\)
  • \(a_l\lt m \lt a_r\)\(a_r\lt m \lt a_l\),则贡献为\(p2\)
  • 其余情况贡献为\(0\)

给定\(m\)个询问,每次询问要求一个区间的所有子区间的贡献之和。

\(n,m\le 2\times 10^5\)


tag:线段树,树状数组

本题有一个部分分\(p1=2p2\),对于该部分分,容易发现可以把最大值和\(l,r\)的关系分开考虑。具体地,对每个\(l\),若\(\max_{i=l+1}^{r-1}a_i \lt a_l\),则\([l,r]\)提供\(p2\)的贡献。对\(r\)同理,两部分贡献可以直接累加。

那么,用单调栈求出\(a_i\)前一个,后一个比它大的位置\(lst_i,nxt_i\)。对询问按右端点排序,从左往右扫描\(r\),给区间\([r+1,nxt_i]\)的值加\(p2\),并处理右端点为\(r\)的所有询问即可。

一般情况,我们只要再求出贡献为\(p1\)的区间即可。考虑最大值\(a_i\),它能作为最大值的区间\([l,r]\)满足\(lst_i\lt l,r\lt nxt_i\)。又要满足\(a_i \le a_l,a_i \le a_r\)的条件,所以题中\(m=a_i\)的区间只有\([lst_i,nxt_i]\)

为统计某个询问区间中有多少个上述区间,可以使用树状数组套线段树,比较套路。

难度Medium,考虑最大值的位置是基本的,但是考场上脑子一抽想统计贡献为 \(p2\) 甚至没有贡献的区间数,然后做不出来。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+10;
int n,m,a[N],st[N],top,x[N],y[N];ll ans[N],p1,p2;
struct query{int l,r,id;}q[N];
bool cmp1(query a,query b){return a.r<b.r;}
bool cmp2(query a,query b){return a.l>b.l;}
int tot,lc[N*60],rc[N*60];ll sum[N*60],tag[N*60];
#define mid ((l+r)>>1)
int new_node(){
	++tot;
	lc[tot]=rc[tot]=sum[tot]=tag[tot]=0;
	return tot;
}
void push_up(int p){sum[p]=sum[lc[p]]+sum[rc[p]];}
void push_down(int p,int l,int r){
	int v=tag[p];tag[p]=0;
	if(!v||l==r)return;
	sum[lc[p]]+=(mid-l+1)*v;tag[lc[p]]+=v;
	sum[rc[p]]+=(r-mid)*v;tag[rc[p]]+=v;
}
void modify(int p,int l,int r,int L,int R,int v){
	if(l>=L&&r<=R){sum[p]+=(r-l+1)*v;tag[p]+=v;return;}
	if(!lc[p])lc[p]=new_node();if(!rc[p])rc[p]=new_node();
	push_down(p,l,r);
	if(L<=mid)modify(lc[p],l,mid,L,R,v);
	if(R>mid)modify(rc[p],mid+1,r,L,R,v);
	push_up(p);
}
ll query(int p,int l,int r,int L,int R){
	if(!p)return 0;
	if(l>=L&&r<=R)return sum[p];
	push_down(p,l,r);
	if(R<=mid)return query(lc[p],l,mid,L,R);
	if(L>mid)return query(rc[p],mid+1,r,L,R);
	return query(lc[p],l,mid,L,R)+query(rc[p],mid+1,r,L,R);
}
#undef mid
void add(int x,int v){for(;x<=n;x+=x&-x)modify(x,1,n,v,v,1);}
int ask(int x,int y){int res=0;for(;x;x-=x&-x)res+=query(x,1,n,y,n);return res;}
int main(){
	scanf("%d%d%lld%lld",&n,&m,&p1,&p2);
	for(int i=1;i<=n;i++)scanf("%d",a+i);
	for(int i=1;i<=m;i++)scanf("%d%d",&q[i].l,&q[i].r),q[i].id=i;
	sort(q+1,q+m+1,cmp1);new_node();
	for(int i=1,j=1;i<=n;i++){
		while(top&&a[st[top]]<a[i])--top;
		modify(1,1,n+2,st[top]+1,i,1);
		x[i]=st[top];st[++top]=i;
		for(;j<=m&&q[j].r<=i;++j)
			ans[q[j].id]+=query(1,1,n+2,q[j].l+1,q[j].r+1);
	}tot=0;new_node();top=0;st[0]=n+1;
	sort(q+1,q+m+1,cmp2);
	for(int i=n,j=1;i>=1;i--){
		while(top&&a[st[top]]<a[i])--top;
		modify(1,1,n+2,i+2,st[top]+1,1);
		y[i]=st[top];st[++top]=i;
		for(;j<=m&&q[j].l>=i;++j)
			ans[q[j].id]+=query(1,1,n+2,q[j].l+1,q[j].r+1);
	}
	tot=0;
	for(int i=1;i<=n;i++)new_node();
	for(int i=1;i<=n;i++)if(x[i]<y[i]&&x[i]!=0&&y[i]!=n+1)add(y[i],x[i]);
	for(int i=1;i<=m;i++){
		int id=q[i].id,l=q[i].l,r=q[i].r;
		int cnt1=ask(r,l)+r-l,cnt2=ans[id]-2*cnt1;
		ans[id]=p1*cnt1+p2*cnt2;
	}
	for(int i=1;i<=m;i++)printf("%lld\n",ans[i]);
	return 0;
}

C 礼物

给定两个长为\(n\)的数列\(x,y\),所有数为\(1,2,...,m\)中的整数。可以将一个数列中所有数加上一个常数,也可以对一个数列做轮换。最小化\(\sum_{i=1}^{n}(x_i-y_i)^2\)

\(n \le 5 \times 10^4,m \le 100\)


tag:FFT

假设在数列\(x\)上加上\(c\),则目标函数化为

\[\sum_{i=1}^{n}((x_i+c)-y_i)^2=\sum_{i=1}^{n}(x_i^2+y_i^2)+\sum_{i=1}^{n}x_iy_i+nc^2-2\sum_{i=1}^{n}(x_i-y_i)\times c \]

关于\(c\)的部分是二次函数,且与\(x\)是否轮换无关,所以可以直接求出\(c\)

剩下的部分是最小化\(\sum_{i=1}^{n}x_iy_i\),显然想到卷积形式,用FFT做即可。

但是!我还不会FFT,所以我使用\(O(n^2)\)的暴力。毕竟\(n\)也不大,卡卡常就过了。最慢的点跑了939ms(时限1s),喜提洛谷最劣解。

难度???,应该是板的,但是我不会板,但是我会暴力。暴力场切。

点击查看代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
inline int read(){
	int x=0;char ch=getchar();
	while(ch<'0'||ch>'9')ch=getchar();
	while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return x;
}
typedef long long ll;
const int N=5e4+5;
int n,m,x[N<<1],y[N],sum,c,ans,res;
int main(){
	n=read();m=read();
	for(register int i=0;i<n;++i)x[n+i]=x[i]=read(),sum+=x[i],ans+=x[i]*x[i];
	for(register int i=0;i<n;++i)y[i]=read(),sum-=y[i],ans+=y[i]*y[i];
	c=floor(-1.0*sum/n+0.5);ans+=n*c*c+2*c*sum;
	for(register int i=0;i<n;++i){
		sum=0;
		for(register int j=0;j<n;++j)sum+=x[i+j]*y[j];
		res=max(res,sum);
	}
	printf("%d\n",ans-2*res);
	return 0;
}

HNOI2018 Day1

2023-05-30


A 寻宝游戏

给定\(n\)个长为\(m\)的二进制串,可以在每两个二进制数之间和第一个数之前添加一个运算符(按位与/按位或),然后在这个算式最前面填上\(0\)。问使得这个算式的值刚好为给定的询问值的添加方法数。\(q\)组询问。答案对\(10^9+7\)取模。

\(n \le 1000, m \le 5000, q \le 1000\)


tag:思维题

将按位与看做\(1\),按位或看做\(0\),会发现:将操作序列和每个数第\(j\)位构成的序列从后往前比较,若操作序列字典序小,则该位结果为\(1\),否则结果为\(0\)。证明是不难的。

那么只要将每一位构成的序列排序,如果询问串要求比一段前缀大,比一段后缀小就合法,输出分界点的两个数的差;否则无解。

用基数排序可以将时间复杂度做到\(O((n+q)m)\)

难度Hard-。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1005,M=5005,mod=1e9+7;
int n,m,q,d=1,cnt[2],val[M],A[M],B[M];char s[M];
int main(){
	scanf("%d%d%d",&n,&m,&q);
	for(int i=1;i<=m;i++)A[i]=i;
	for(int i=1;i<=n;i++,d=d*2%mod){
		scanf("%s",s+1);
		cnt[0]=0;cnt[1]=m;
		for(int j=1;j<=m;j++){
			if(s[j]=='0')cnt[0]++;
			else val[j]=(val[j]+d)%mod;
		}
		for(int j=m;j>=1;j--)
			B[cnt[s[A[j]]-'0']--]=A[j];
		for(int j=1;j<=m;j++)A[j]=B[j];
	}
	for(int i=1;i<=m;i++)B[A[i]]=i;
	A[m+1]=m+1;val[m+1]=d;
	for(int i=1;i<=q;i++){
		int L=0,R=m+1;
		scanf("%s",s+1);
		for(int j=1;j<=m;j++){
			if(s[j]=='0')L=max(L,B[j]);
			else R=min(R,B[j]);
		}
		if(L>R)printf("0\n");
		else printf("%d\n",(val[A[R]]-val[A[L]]+mod)%mod);
	}
	return 0;
}

B 转盘

一个转盘上有摆成一圈的\(n\)个物品,依次编号为\(1\sim n\),编号为的\(i\)物品会在\(T_i\)时刻出现(之后不消失)。在 \(0\) 时刻时,小G可以任选 \(n\) 个物品中的一个,每经过一个单位时间可以继续选择当前物品或选择下一个物品。在每一时刻,如果小 G 选择的物品已经出现了,那么小G将会标记它。问小G至少需要多少时间来标记所有物品。

然而还有\(m\)次修改,每次修改改变一个物品的出现时间。强制在线。

\(n,m\le 10^5\)


建议BC两题换一下名字

tag:线段树,单调栈

首先转化问题,记\(a_i=t_i-i (1 \le i \le 2n),t_{i+n}=t_i\),注意到小G最多转一圈,随便推推容易发现:以\(i\)为起点的答案是\(\max_{i\le j \lt i+n}{a_j}+n+i-1\)。然后可以把上界换成\(2n\)

这个时候转化视角,考虑对于某个\(j\)的最小值。显然只用考虑\(a_j\)是后缀最大值的情形。设\(a_i\)是大于\(a_j\)的数中最靠右的一个,则\(a_j\)的答案是\(a_j+i+n\)

所以可以考虑用线段树维护一个单调栈,从后向前考虑即可。

难度Hard。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,INF=1<<30;
int n,m,op,a[N<<1],ans;
struct node{int mx,res;}tr[N<<2];
int query(int p,int l,int r,int x){
	if(l==r)return tr[p].mx>x?x+l:INF;
	int mid=l+r>>1;
	if(tr[p<<1|1].mx<=x)return query(p<<1,l,mid,x);
	return min(tr[p].res,query(p<<1|1,mid+1,r,x));
}
void push_up(int p,int l,int r){
	tr[p].mx=max(tr[p<<1].mx,tr[p<<1|1].mx);
	tr[p].res=query(p<<1,l,l+r>>1,tr[p<<1|1].mx);
}
void build(int p,int l,int r){
	if(l==r){tr[p].mx=a[l]-l;return;}
	int mid=l+r>>1;
	build(p<<1,l,mid);
	build(p<<1|1,mid+1,r);
	push_up(p,l,r);
}
void modify(int p,int l,int r,int x,int v){
	if(l==r){tr[p].mx=v-l;return;}
	int mid=l+r>>1;
	if(x<=mid)modify(p<<1,l,mid,x,v);
	else modify(p<<1|1,mid+1,r,x,v);
	push_up(p,l,r);
}
int main(){
	scanf("%d%d%d",&n,&m,&op);
	for(int i=1;i<=n;i++)scanf("%d",a+i);
	build(1,1,n);
	printf("%d\n",ans=query(1,1,n,tr[1].mx-n)+n);
	for(int i=1,x,y;i<=m;i++){
		scanf("%d%d",&x,&y);
		if(op)x^=ans,y^=ans;
		modify(1,1,n,x,y);
		printf("%d\n",ans=query(1,1,n,tr[1].mx-n)+n);
	}
	return 0;
}

C 毒瘤

\(n\)个点,\(m\)条边的连通简单无向图,求独立集个数。

\(n\le 10^5,m \le n+10\)


tag:DP,虚树/动态DP

首先随便取一棵生成树,然后标记所有非树边的端点。

对于树的情况,很容易用树形DP解决。同时有一个显然的暴力:枚举上述所有端点的颜色,做树形DP。

考虑对所有端点建出虚树,在虚树上DP。这样时间复杂度就是\(O(s4^s)\),其中\(s=m-n+1\),能过。

发现在虚树上的一条边转移时,不论该边两个端点状态如何,因为这两个点在原树上之间的形态相同,所以转移系数是相同的。那么暴力求出每个点到它虚树上的父亲的转移系数。这里可以直接暴力,最多把每个点遍历一次。

剩下就是二进制枚举。

这道题也可以用动态DP,可以算是一个模板题。

难度Hard-。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+50,mod=998244353;
int n,m,ans,a[N];
int head[N],ver[N<<1],nxt[N<<1],tot=1;
int hc[N],vc[N<<1],nc[N<<1],tc;
void add(int u,int v){ver[++tot]=v;nxt[tot]=head[u];head[u]=tot;}
void addc(int u,int v){vc[++tc]=v;nc[tc]=hc[u];hc[u]=tc;}
int fa[N][20],dep[N],dfn[N],Dfn,tr[N<<1],vis[N],x[N],d[N],k,st[N],top;
void dfs(int u){
	dfn[u]=++Dfn;
	for(int i=head[u],v;i;i=nxt[i])
		if(!dfn[v=ver[i]]){
			tr[i]=tr[i^1]=1;
			fa[v][0]=u;dep[v]=dep[u]+1;
			dfs(v);
		}
}
void LCA_pre(){
	for(int j=1;(1<<j)<=n;j++)
		for(int i=1;i<=n;i++)
			fa[i][j]=fa[fa[i][j-1]][j-1];
}
int LCA(int u,int v){
	if(dep[u]<dep[v])swap(u,v);
	int d=dep[u]-dep[v];
	for(int i=17;i>=0;i--)
		if(d&(1<<i))u=fa[u][i];
	if(u==v)return u;
	for(int i=17;i>=0;i--)
		if(fa[u][i]!=fa[v][i])
			u=fa[u][i],v=fa[v][i];
	return fa[u][0];
}
bool cmp(int i,int j){return dfn[i]<dfn[j];}
void vtree(){
	sort(x+1,x+k+1,cmp);st[top=1]=1;
	for(int i=1;i<=k;i++)if(x[i]!=1){
		int g=LCA(x[i],st[top]);
		if(g!=st[top]){
			while(dfn[g]<dfn[st[top-1]])
				addc(st[top-1],st[top]),--top;
			if(dfn[g]!=dfn[st[top-1]])
				addc(g,st[top]),st[top]=g;
			else addc(g,st[top]),--top;
		}
		st[++top]=x[i];
	}
	for(int i=1;i<top;i++)addc(st[i],st[i+1]);
}
int dp[N][2],trans[N][2][2],f[N][2];
void DP_pre(int u){
	dp[u][0]=dp[u][1]=1;
	for(int i=head[u],v;i;i=nxt[i])
		if(tr[i]&&(v=ver[i])!=fa[u][0]){
			DP_pre(v);
			if(!vis[v]){
				dp[u][0]=1ll*dp[u][0]*(dp[v][0]+dp[v][1])%mod;
				dp[u][1]=1ll*dp[u][1]*dp[v][0]%mod;
			}
			else vis[u]=1;
		}
}
void DP(int u){
	f[u][0]=dp[u][0];f[u][1]=dp[u][1];
	if(a[u]!=-1)f[u][1-a[u]]=0;
	for(int i=hc[u],v;i;i=nc[i]){
		DP(v=vc[i]);
		int f0=(1ll*trans[v][0][0]*f[v][0]+1ll*trans[v][0][1]*f[v][1])%mod,
			f1=(1ll*trans[v][1][0]*f[v][0]+1ll*trans[v][1][1]*f[v][1])%mod;
		f[u][0]=1ll*f[u][0]*(f0+f1)%mod;
		f[u][1]=1ll*f[u][1]*f0%mod;
	}
}
int main(){
	memset(a,-1,sizeof(a));
	scanf("%d%d",&n,&m);
	for(int i=1,u,v;i<=m;i++){
		scanf("%d%d",&u,&v);
		add(u,v);add(v,u);
	}
	dfs(1);LCA_pre();
	for(int i=2;i<=tot;i+=2)if(!tr[i]){
		++k;vis[d[k]=x[k]=ver[i]]=1;
		++k;vis[d[k]=x[k]=ver[i^1]]=1;
	}
	DP_pre(1);
	sort(x+1,x+k+1);
	k=unique(x+1,x+k+1)-x-1;
	vtree();
	for(int u=1;u<=n;u++)
		for(int i=hc[u];i;i=nc[i]){
			int v=vc[i];
			trans[v][0][0]=trans[v][1][1]=1;
			for(int x=v;fa[x][0]!=u;x=fa[x][0]){
				int t00=trans[v][0][0],t01=trans[v][0][1],
					t10=trans[v][1][0],t11=trans[v][1][1],y=fa[x][0];
				trans[v][0][0]=1ll*dp[y][0]*(t00+t10)%mod;
				trans[v][0][1]=1ll*dp[y][0]*(t01+t11)%mod;
				trans[v][1][0]=1ll*dp[y][1]*t00%mod;
				trans[v][1][1]=1ll*dp[y][1]*t01%mod;
			}
		}
	for(int i=0;i<(1<<k);i++){
		for(int j=1;j<=k;j++)a[x[j]]=(i>>j-1)&1;
		bool flag=1;
		for(int j=1;j<=m-n+1;j++)
			if(a[d[2*j-1]]&&a[d[2*j]])flag=0;
		if(!flag)continue;
		DP(1);ans=(ans+(f[1][0]+f[1][1])%mod)%mod;
	}
	printf("%d\n",ans);
	return 0;
}

动态DP:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+50;const ll mod=998244353;
int n,m;ll f[N][2],ans;
int head[N],nxt[N<<1],ver[N<<1],tot=1,tr[N<<1];
void add(int u,int v){ver[++tot]=v;nxt[tot]=head[u];head[u]=tot;}
ll power(ll a,ll b){
	ll c=1;
	for(;b;b>>=1){
		if(b&1)c=c*a%mod;
		a=a*a%mod;
	}
	return c;
}
struct Matrix{
	ll a[2][2];
	Matrix (){a[0][0]=a[0][1]=a[1][0]=a[1][1]=0;}
	Matrix operator *(const Matrix&b)const{
		Matrix c;
		c.a[0][0]=(a[0][0]*b.a[0][0]+a[0][1]*b.a[1][0])%mod;
		c.a[1][0]=(a[1][0]*b.a[0][0]+a[1][1]*b.a[1][0])%mod;
		c.a[0][1]=(a[0][0]*b.a[0][1]+a[0][1]*b.a[1][1])%mod;
		c.a[1][1]=(a[1][0]*b.a[0][1]+a[1][1]*b.a[1][1])%mod;
		return c;
	}
}g[N];
int fa[N],top[N],siz[N],L[N],dfn,R[N],son[N],id[N];
void dfs(int u){
	f[u][0]=f[u][1]=1;siz[u]=1;
	for(int i=head[u],v;i;i=nxt[i])
		if(!siz[v=ver[i]]){
			tr[i]=tr[i^1]=1;
			fa[v]=u;dfs(v);siz[u]+=siz[v];
			if(siz[v]>siz[son[u]])son[u]=v;
			f[u][0]=f[u][0]*(f[v][0]+f[v][1])%mod;
			f[u][1]=f[u][1]*f[v][0]%mod;
		}
}
void rdfs(int u,int tp){
	g[u].a[0][0]=g[u].a[1][0]=1;
	L[u]=++dfn;id[dfn]=u;top[u]=tp;R[tp]=dfn;
	if(son[u])rdfs(son[u],tp);
	for(int i=head[u],v;i;i=nxt[i])
		if(tr[i]&&(v=ver[i])!=fa[u]&&v!=son[u]){
			rdfs(v,v);
			g[u].a[0][0]=g[u].a[0][0]*(f[v][0]+f[v][1])%mod;
			g[u].a[1][0]=g[u].a[1][0]*f[v][0]%mod;
		}
	g[u].a[0][1]=g[u].a[0][0];
}
struct SegmentTree{
	Matrix a[N<<2];
	#define mid (l+r>>1)
	void build(int p,int l,int r){
		if(l==r){a[p]=g[id[l]];return;}
		build(p<<1,l,mid);
		build(p<<1|1,mid+1,r);
		a[p]=a[p<<1]*a[p<<1|1];
	}
	void modify(int p,int l,int r,int x){
		if(l==r){a[p]=g[id[l]];return;}
		if(x<=mid)modify(p<<1,l,mid,x);
		else modify(p<<1|1,mid+1,r,x);
		a[p]=a[p<<1]*a[p<<1|1];
	}
	Matrix query(int p,int l,int r,int L,int R){
		if(l>=L&&r<=R)return a[p];
		if(R<=mid)return query(p<<1,l,mid,L,R);
		if(L>mid)return query(p<<1|1,mid+1,r,L,R);
		return query(p<<1,l,mid,L,R)*query(p<<1|1,mid+1,r,L,R);
	}
	#undef mid
}seg;
int a[N],x[50],k,st[N],Top;Matrix g0[N];
vector<int> rej[N];
void update(int u,int op){
	st[++Top]=u,g0[Top]=g[u];
	if(op==0)g[u].a[1][0]=0;
	else g[u].a[0][0]=g[u].a[0][1]=0;
	while(u){
		Matrix lst=seg.query(1,1,n,L[top[u]],R[top[u]]);
		seg.modify(1,1,n,L[u]);
		Matrix now=seg.query(1,1,n,L[top[u]],R[top[u]]);
		u=fa[top[u]];st[++Top]=u,g0[Top]=g[u];
		g[u].a[0][0]=g[u].a[0][0]*(now.a[0][0]+now.a[1][0])%mod
					 *power(lst.a[0][0]+lst.a[1][0],mod-2)%mod;
		g[u].a[1][0]=g[u].a[1][0]*now.a[0][0]%mod
					 *power(lst.a[0][0],mod-2)%mod;
		g[u].a[0][1]=g[u].a[0][0];
	}
}
void clear(int tim){
	while(Top>tim){
		g[st[Top]]=g0[Top];
		seg.modify(1,1,n,L[st[Top]]);
		--Top;
	}
}
void Enum(int p){
	if(p==k+1){
		Matrix res=seg.query(1,1,n,1,R[1]);
		ans=(ans+res.a[0][0]+res.a[1][0])%mod;
		return;
	}
	int tim=Top;
	update(x[p],a[x[p]]=0);Enum(p+1);clear(tim);a[x[p]]=-1;
	update(x[p],a[x[p]]=1);bool flag=1;
	for(auto t:rej[x[p]])if(a[t]==1){flag=0;break;}
	if(flag)Enum(p+1);clear(tim);a[x[p]]=-1;
}
int main(){
	memset(a,-1,sizeof(a));
	scanf("%d%d",&n,&m);
	for(int i=1,u,v;i<=m;i++){
		scanf("%d%d",&u,&v);
		add(u,v);add(v,u);
	}
	dfs(1);rdfs(1,1);seg.build(1,1,n);
	for(int i=2;i<=tot;i+=2)if(!tr[i]){
		x[++k]=ver[i];x[++k]=ver[i^1];
		rej[ver[i]].push_back(ver[i^1]);
		rej[ver[i^1]].push_back(ver[i]);
	}
	sort(x+1,x+k+1);k=unique(x+1,x+k+1)-x-1;
	Enum(1);
	printf("%lld\n",ans);
	return 0;
}

USACO23open选做

2023-05-22


A Custodial Cleanup G

\(n\) 个点 \(m\) 条边的无向图,点 \(i\) 有一个颜色 \(c_i\),一把颜色为 \(s_i\) 的钥匙,目标是在该点放置颜色为 \(f_i\) 的钥匙。FJ初始在点 \(1\),他可以捡起当前点的钥匙,放下一把或多把钥匙,以及移动向相邻的点。这里能够移动的条件是FJ手中至少有一把和目标点颜色相同的钥匙。问FJ能否完成目标。\(T\) 组数据。

\(0 \le m \le 10^5\)\(1 \le c_i,s_i,f_i \le n \le 10^5\)
\(1 \le T \le 100\)\(1 \le \sum n \le 10^5\)\(1 \le \sum m \le 2 \times 10^5\)


tag:BFS
(其实这个tag也无所谓,关键是分析)

首先考虑FJ至多能捡起多少钥匙,此时当然钦定FJ不放下任何钥匙。做一个BFS,不断扩展可以到达的连通块。扩展过程中,维护已经拿到的钥匙(用一个bool数组),并记录各个颜色相邻的点(用\(n\)个vector)。每次取出新点,检查是否获得该颜色的钥匙,如果没有则更新,并将该颜色的vector中所有点加入队列。然后扩展该点的邻点,如果颜色可以走到就加入队列,否则加入相应的vector。

在过程中,我们求出了FJ能到达的所有点。在这些点中,再来考虑FJ能放下哪些钥匙。假设FJ可以完成目标,让FJ倒着运动,即初始时点 \(i\) 有一把颜色为 \(f_i\) 的钥匙,此时FJ可以进入一个点,当且仅当他有该点颜色的钥匙,或者该点颜色的钥匙就放在该房间里。同样的做一遍BFS即可。

对于第二遍BFS中不能到达的点,检查是否有 \(s_i==f_i\) 成立。若有不成立者,则答案为NO,否则为YES。

难度Easy+,场切。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int T,n,m,c[N],s[N],t[N];
int head[N],nxt[N<<1],ver[N<<1],tot;
void add(int u,int v){ver[++tot]=v;nxt[tot]=head[u];head[u]=tot;}
int vis1[N],vis2[N],tag[N];
queue<int> Q;vector<int> col[N];
int main(){
	scanf("%d",&T);
	while(T--){
		scanf("%d%d",&n,&m);tot=0;
		for(int i=1;i<=n;i++){col[i].clear();head[i]=vis1[i]=tag[i]=0;}
		for(int i=1;i<=n;i++)scanf("%d",c+i);
		for(int i=1;i<=n;i++)scanf("%d",s+i);
		for(int i=1;i<=n;i++)scanf("%d",t+i);
		for(int i=1,u,v;i<=m;i++){scanf("%d%d",&u,&v);add(u,v);add(v,u);}
		Q.push(1);
		while(!Q.empty()){
			int u=Q.front();Q.pop();vis1[u]=1;
			if(!tag[s[u]]){
				for(auto x:col[s[u]])Q.push(x);
				col[s[u]].clear();tag[s[u]]=1;
			}
			for(int i=head[u],v;i;i=nxt[i])
				if(!vis1[v=ver[i]]){
					if(tag[c[v]])Q.push(v);
					else col[c[v]].push_back(v);
				}
		}
		for(int i=1;i<=n;i++){col[i].clear();vis2[i]=tag[i]=0;}
		Q.push(1);
		while(!Q.empty()){
			int u=Q.front();Q.pop();vis2[u]=1;
			if(!tag[t[u]]){
				for(auto x:col[t[u]])Q.push(x);
				col[t[u]].clear();tag[t[u]]=1;
			}
			for(int i=head[u],v;i;i=nxt[i])
				if(vis1[v=ver[i]]&&!vis2[v]){
					if(tag[c[v]]||c[v]==t[v])Q.push(v);
					else col[c[v]].push_back(v);
				}
		}
		bool flag=1;
		for(int i=1;i<=n;i++)if(!vis2[i]&&s[i]!=t[i])flag=0;
		printf(flag?"YES\n":"NO\n");
	}
	return 0;
}

B Tree Merging G

定义一棵有根树的一次合并操作为:选择两个具有相同父亲的结点,将其合并成一个节点,新节点的编号为原来的两个节点编号的较大值,新节点的子节点集合为原来的两个节点子节点集合的并集。

给定初始状态和最终状态,构造操作序列。保证有解。初始状态和最终状态分别是 \(n\)\(m\) 个点的树。\(T\) 组数据。

\(1\le t \le 100\)\(2 \le m \le n \le 1000\)


tag:思维题

按照深度处理。

假设深度较小的所有合并已经正确维护,考虑某一个节点的所有儿子。那些不在最终树中的点将被合并。

Case 1:如果该点的子树中有点被保留,那么找到被保留的这个点的某个祖先与之深度相同,合并。

Case 2:否则,找到一个点,最终树中其子树往下每层的最大值都大于该点的最大值,合并。

所有步骤都可以暴力,我甚至还用了一个set。看起来复杂度不太对,但是应该不好卡。

难度Medium-,场切。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int T,n,m,rt,f[N],g[N],r[N],mx[N][N],mx0[N][N];
set<int> a[N],b[N],s[N];
vector<pair<int,int> > ans;
void merge(int x,int y){
	ans.push_back(make_pair(x,y));
	for(auto p:a[x])a[y].insert(p);a[x].clear();
	for(auto p:s[x])s[y].insert(p);s[x].clear();
	for(int i=1;mx[y][i];i++)mx[y][i]=max(mx[y][i],mx[x][i]);
}
void dfsa(int u){
	s[u].clear();s[u].insert(u);mx[u][0]=u;
	for(auto v:a[u]){
		dfsa(v);
		for(auto x:s[v])s[u].insert(x);
		for(int i=0;mx[v][i];i++)
			mx[u][i+1]=max(mx[u][i+1],mx[v][i]);
	}
}
void dfsb(int u){
	mx0[u][0]=u;
	for(auto v:b[u]){
		dfsb(v);
		for(int i=0;mx0[v][i];i++)
			mx0[u][i+1]=max(mx0[u][i+1],mx0[v][i]);
	}
}
void dfs(int u){
	for(auto x:a[u])if(b[u].find(x)==b[u].end()){
		int y=0;
		for(auto z:s[x])if(r[z]){y=z;break;}
		if(y){
			for(;b[u].find(y)==b[u].end();y=g[y]);
			merge(x,y);continue;
		}
		for(auto y:b[u]){
			bool flag=1;
			for(int i=0;mx[x][i];i++)if(mx0[y][i]<mx[x][i])flag=0;
			if(flag){merge(x,y);break;}
		}
	}
	for(auto v:b[u])dfs(v);
}
int main(){
	scanf("%d",&T);
	while(T--){
		scanf("%d",&n);
		ans.clear();
		for(int i=1;i<=n;i++){
			a[i].clear(),b[i].clear(),r[i]=f[i]=0;
			for(int j=0;mx[i][j];j++)mx[i][j]=0;
			for(int j=0;mx0[i][j];j++)mx0[i][j]=0;
		}
		for(int i=1,u,v;i<n;i++){
			scanf("%d%d",&u,&v);
			a[v].insert(u);f[u]=v;
		}
		scanf("%d",&m);
		for(int i=1,u,v;i<m;i++){
			scanf("%d%d",&u,&v);
			b[v].insert(u);g[u]=v;r[u]=r[v]=1;
		}
		for(int i=1;i<=n;i++)if(!f[i])rt=i;
		dfsa(rt);dfsb(rt);dfs(rt);
		printf("%d\n",ans.size());
		for(auto x:ans)printf("%d %d\n",x.first,x.second);
	}
	return 0;
}

C Pareidolia P

对于一个字符串,其权值为满足下面条件的 \(k\) 的最大值:字符串bessie重复 \(k\) 次后是该字符串的子串。

给定一个长为 \(n\) 的字符串 \(S\)\(m\) 次修改字符,求所有修改前和每次修改后,字符串所有子串的权值之和。

\(1 \le n,m \le 2 \times 10^5\)


tag:DP,线段树

首先用DP处理静态问题。记 \(T=\)bessie,下标从 \(0\) 开始。用 \(f_{i,j}\) 表示以 \(S_i\) 结尾,处理到 \(T_j\) 的子串数目。转移如下:

\(f_{i-1,j} \to f_{i,j+[s_i=t_j]} , 1 \to f_{i,[s_i=t_0]} , ([s_i=t_5])f_{i-1,5} \to ans\)

用线段树维护这个转移,对每个区间,需要记录:

  • \(T_j\) 进入,离开的字符 \(nxt_j\)
  • 离开的字符是 \(T_j\) 的后缀数 \(cnt_j\)
  • 进入的字符是 \(T_j\) 的贡献位置数 \(res_j\)

合并时,用左边的 \(cnt_j\) 与右边的 \(res_j\) 相乘更新答案。

难度Medium+。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+5;
int n,m;char s[N];
string t="bessie";
struct node{ll nxt[6],cnt[6],res[6],sum;}tr[N<<2];
node gen(char ch,int pos){
	node x;x.sum=0;
	for(int i=0;i<6;i++)x.nxt[i]=x.cnt[i]=x.res[i]=0;
	if(pos){
		for(int i=0;i<6;i++)x.nxt[i]=(ch==t[i]?(i+1)%6:i);
		x.cnt[x.nxt[0]]=1;x.res[5]=(ch=='e'?n-pos+1:0);
	}
	return x;
}
void push_up(int p){
	node a=tr[p<<1],b=tr[p<<1|1],c=gen('#',0);
	c.sum=a.sum+b.sum;
	for(int i=0;i<6;i++){
		c.nxt[i]=b.nxt[a.nxt[i]];
		c.cnt[i]+=b.cnt[i];c.cnt[b.nxt[i]]+=a.cnt[i];
		c.res[i]=b.res[a.nxt[i]]+a.res[i];
		c.sum+=a.cnt[i]*b.res[i];
	}
	tr[p]=c;
}
void build(int p,int l,int r){
	if(l==r){tr[p]=gen(s[l],l);return;}
	build(p<<1,l,l+r>>1);
	build(p<<1|1,(l+r>>1)+1,r);
	push_up(p);
}
void modify(int p,int l,int r,int x,char c){
	if(l==r){tr[p]=gen(c,x);return;}
	int mid=l+r>>1;
	if(x<=mid)modify(p<<1,l,mid,x,c);
	else modify(p<<1|1,mid+1,r,x,c);
	push_up(p);
} 
int main(){
	scanf("%s",s+1);n=strlen(s+1);
	build(1,1,n);
	printf("%lld\n",tr[1].sum);
	scanf("%d",&m);
	for(int i=1;i<=m;i++){
		int x;char c[2];
		scanf("%d%s",&x,c);
		modify(1,1,n,x,c[0]);
		printf("%lld\n",tr[1].sum);
	}
	return 0;
}

D Triples of Cows P

给定一棵初始有 \(n\) 个点的树。在第 \(i\) 天,这棵树的第 \(i\) 个点会被删除,所有与点 \(i\) 直接相连的点之间都会两两连上一条边。在每次删点前,求出满足 \((a,b)\) 之间有边,\((b,c)\) 之间有边且 \(a\not=c\) 的有序三元组 \((a,b,c)\) 数。

\(n \le 2 \times 10^5\)


tag:拆边,并查集

由于每次删点之后新增的边数太多,所以考虑拆边。用一个白点表示一条边,并将原来树中的点称为黑点,编号不变。对于第 \(i\) 条边 \((u_i,v_i)\),在 \((u_i,n+i),(v_i,n+i)\) 间连边,得到一棵树 \(T\)

删除点 \(i\) 时,我们将点 \(i\) 相邻的所有白点合并为一个点,然后删除 \(i\)。容易归纳证明:每次删除后 \(T\) 仍然是树;真实的 \(u,v\) 间有边等价于树 \(T\) 中存在一个白点与 \(u,v\) 均相邻。

因为依次删除 \(1,2,...,n\),所以在树 \(T\) 中,可以把点 \(n\) 作为根。这样每次删除时,就可以把 \(i\) 的所有相邻白点合并到 \(i\) 的父亲白点上去。然后,用并查集维护每个白点被合并到了哪个点。

\(fa_u\) 表示在初始的 \(T\)\(u\) 的父亲结点,\(p_u\) 表示某个时刻白点 \(u\) 被合并到的点。那么,在这一时刻,黑点 \(u\) 的父亲结点是 \(p_{fa_u}\),白点 \(u\) 的父亲节点是 \(fa_{p_u}\)

下面来考虑如何统计答案。对白点 \(u\),记 \(s_u\)\(u\) 的儿子个数。

对于一个符合要求的 \((a,b,c)\),设 \(a,b\) 通过白点 \(x\) 相连,\(b,c\) 通过白点 \(y\) 相连。

  1. 如果 \(x=y\):固定 \(x\),在 \(x\) 的邻点中任选 \(3\) 个,则对答案的贡献为

    \[\sum_{x} (s_x+1)s_x(s_x-1) \]

    求和的条件是 \(x\) 是白点。
  2. 如果 \(x\not=y\),且 \(x,y\) 都是 \(b\) 的子节点:固定 \(b\),先任取 \(b\) 的两个子结点 \(x,y\)(有序),此时贡献 \(s_xs_y\)。则总的贡献为

    \[\sum_{b} (\sum_{x} s_x) ^2 - \sum_{x} s_x^2 \]

    第一个求和的条件是 \(b\) 是黑点,后两个求和的条件是 \(x\)\(b\) 的儿子。
    注意到后一项拆出来就是对所有白点 \(x\),求 \(s_x^2\) 的和,那就拆出来吧。
  3. 如果 \(x\not=y\),且 \(x,y\) 一个是 \(b\) 的子结点,另一个是 \(b\) 的父亲结点:不妨 \(x\)\(b\) 的父亲结点,固定 \(x\)\(b\)\(x\) 的一个子结点,\(y\) 又是 \(b\) 的一个子结点,则对答案的贡献为

    \[\sum_{x} 2\times s_x \times \sum_{b} \sum_{y} s_y \]

    三个求和的条件分别为 \(x\) 是白点,\(b\)\(x\) 的子结点,\(y\)\(b\) 的结点。

列出式子后,我们发现需要维护以下数据:

  1. 白点 \(u\) 的儿子数目 \(s_u\)
  2. 黑点 \(u\) 的儿子的 \(s\) 值之和,也可以存到数组 \(s\)
  3. 白点 \(u\) 的儿子的 \(s\) 值之和 \(t_u\)
    答案是 \(\sum_{x} (f(s_x)+2s_xt_x)+\sum_{y} s_y^2\),其中 \(f(x)=x^3-x^2-x\),两个求和的条件分别是 \(x\) 是黑点,\(y\)是白点。

删除点 \(u\) 时,枚举它初始的的儿子(一定没有被合并过),在并查集中将其合并到 \(u\) 的父亲中。然后清零 \(u\)\(u\) 的儿子的 \(s,t\) 值,更新 \(u\) 的三层祖先的值,并更新答案。

难度Hard。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=4e5+5;
int n,fa[N],p[N];ll s[N],t[N],ans;
int head[N],ver[N<<1],nxt[N<<1],tot;
void add(int u,int v){ver[++tot]=v;nxt[tot]=head[u];head[u]=tot;}
void dfs(int u){
	for(int i=head[u],v;i;i=nxt[i])
		if((v=ver[i])!=fa[u]){
			fa[v]=u;dfs(v);
			if(u<=n)s[u]+=s[v];
			else ++s[u],t[u]+=s[v];
		}
}
int find(int x){return (x==p[x]?x:(p[x]=find(p[x])));}
ll f(ll x){return x*x*x-x*x-x;}
int main(){
	scanf("%d",&n);
	for(int i=1,u,v;i<n;i++){
		scanf("%d%d",&u,&v);
		add(u,n+i);add(v,n+i);add(n+i,u);add(n+i,v);
	}
	dfs(n);
	for(int i=1;i<2*n;i++)p[i]=i;
	for(int i=1;i<=n;i++)ans+=s[i]*s[i];
	for(int i=n+1;i<2*n;i++)ans+=f(s[i])+2*s[i]*t[i];
	for(int u=1;u<=n;u++){
		printf("%lld\n",ans);
		int g=find(fa[u]),w=fa[g];ll del=-1;
		ans-=f(s[g])+2*s[g]*t[g]+s[w]*s[w];s[w]-=s[g];--s[g];
		t[g]-=s[u];ans-=s[u]*s[u];s[u]=0;
		for(int i=head[u],v;i;i=nxt[i])
			if((v=ver[i])!=fa[u]){
				p[v]=g;s[g]+=s[v];t[g]+=t[v];del+=s[v];
				ans-=f(s[v])+2*s[v]*t[v];s[v]=t[v]=0;
			}
		s[w]+=s[g];ans+=f(s[g])+2*s[g]*t[g]+s[w]*s[w];
		t[w=find(fa[fa[g]])]+=del;ans+=2*s[w]*del;
	}
	return 0;
}

APIO2015

2023-04-20


A 巴厘岛的雕塑

\(n\) 个数分为若干组,组数不少于 \(a\) 且不多于 \(b\)。最小化各组和的 \(OR\) 值。

\(n \le 2000\)\(1=a \le b \le n\)\(n \le 100\)\(1 \le a \le b\)


tag:贪心,DP

按位处理,从高到低依次尝试每一位是否能够是 \(0\)\(DP\) 求出在满足高位的条件下的最少分组数和最多分组数即可。

但似乎这个做法是伪的,uoj上没有过。

重新考虑 \(DP\) 状态,考虑到某一位能否分若干组即可。不想写了。

难度Easy,场切。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2005;
typedef long long ll;
int n,a,b,dp[N][2];ll s[N],res;
int main(){
	scanf("%d%d%d",&n,&a,&b);
	for(int i=1,x;i<=n;i++){
		scanf("%d",&x);
		s[i]=s[i-1]+x;
	}
	res=(1ll<<45)-1;
	for(int d=44;d>=0;d--){
		ll chk=res-(1ll<<d);
		dp[0][0]=dp[0][1]=0;
		for(int i=1;i<=n;i++){
			dp[i][0]=1e9;dp[i][1]=-1e9;
			for(int j=0;j<i;j++)
				if((s[i]-s[j]|chk)==chk){
					dp[i][0]=min(dp[i][0],dp[j][0]+1);
					dp[i][1]=max(dp[i][1],dp[j][1]+1);
				}
		}
		if(dp[n][1]>=a&&dp[n][0]<=b)res=chk;
	}
	printf("%lld\n",res);
	return 0;
}

B 雅加达的摩天楼

\(N\) 座楼排列成一条直线,依次编号为 \(0\)\(N − 1\)。有 \(M\) 只叫做 “doge” 的神秘生物,编号依次是 \(0\)\(M − 1\)。编号为 \(i\) 的 doge 最初在 \(B_i\) 的楼。doge 能够跳跃,编号为 \(i\) 的 doge 的跳跃能力为 \(P_i\)。在一次跳跃中,位于摩天楼 \(b\) 而跳跃能力为 \(p\) 的 doge 可以跳跃到编号为 \(b - p\) (如果 \(0 \leq b - p < N\))或 \(b + p\) (如果 \(0 \leq b + p < N\))的摩天楼。

编号为 \(0\) 的 doge 有一条紧急的消息要尽快传送给编号为 \(1\) 的 doge。任何一个收到消息的 doge 有以下两个选择:

  • 跳跃到其他摩天楼上;
  • 将消息传递给它当前所在的摩天楼上的其他 doge。

计算将消息从 \(0\) 号 doge 传递到 \(1\) 号 doge 所需要的最少总跳跃步数,或者告诉它们消息永远不可能传递到 \(1\) 号 doge。

\(1 \leq N \leq 30000\)\(1 \leq P_i \leq 30000\)\(2 \leq M \leq 30000\)


tag:最短路,建图技巧

以位置为顶点建图,每个点上如果有 doge 就向它能够跳到的点连边。跑 dijkstra 可以求解,但是会 T。

考虑重复的边。一个剩余类中可能有很多 doge,没有必要让它们都连边。事实上,同一个剩余类中只需要在相邻的 doge 间连边即可。此时容易证明边数级别为 \(O(n \sqrt n)\)。时间复杂度为 \(O(n \log n)\)

难度Medium-,场切。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
typedef vector<int> vi;
const int N=3e4+5,M=1e7+5;
int n,m,r,s,t,dis[N],vis[N];
int head[N],nxt[M],ver[M],val[M],tot;
void add(int u,int v,int w){
	ver[++tot]=v;val[tot]=w;
	nxt[tot]=head[u];head[u]=tot;
}
map<pii,vi> MP;vi d[N];
priority_queue<pii> Q;
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1,b,p;i<=m;i++){
		scanf("%d%d",&b,&p);
		if(i==1)s=b;if(i==2)t=b;
		d[b].push_back(p);
		MP[pii(b%p,p)].push_back(b);
	}
	for(auto f:MP){
		vi x=f.second;
		int b=f.first.first,p=f.first.second;
		sort(x.begin(),x.end());int len=x.size();
		for(int i=0;i<len;i++){
			int lst=b,nxt=(n-b)/p*p+b;
			if(i!=0)lst=x[i-1];
			if(i!=len-1)nxt=x[i+1];
			for(int j=lst;j<=nxt;j+=p)
				add(x[i],j,abs(x[i]-j)/p);
		}
	}
	memset(dis,0x3f,sizeof(dis));
	Q.push(make_pair(0,s));dis[s]=0;
	while(!Q.empty()){
		int u=Q.top().second,d=-Q.top().first;Q.pop();
		if(vis[u])continue;vis[u]=1;
		for(int i=head[u],v;i;i=nxt[i])
			if(dis[v=ver[i]]>d+val[i]){
				dis[v]=d+val[i];
				Q.push(make_pair(-dis[v],v));
			}
	}
	if(dis[t]>1e9)printf("-1\n");
	else printf("%d\n",dis[t]);
	return 0;
}

C 巴邻旁之桥

河岸两旁有 \(n\) 个人居住和工作,两岸分别为 A 和 B。每个人有一个居住地和工作地。现在要建 \(k\) 座桥使得所有人上班的总路程最小。桥长为 \(1\)。求最小值。

\(1 \le n \le 10^5\),$ k=1,2 $


tag:中位数

首先不考虑那些居住和工作在同侧的人。对于剩下的人,先加上桥上路程。

\(k=1\) 的情况中,设桥建在位置 \(x\),则目标函数形如 $\sum |x_i-x| $,这里 \(x_i\) 表示所有位置。要最小化目标函数,只用将 \(x\) 取为中位数即可。

\(k=2\) 的情况,对于一个人来说,他应当选择距离自己居住地和工作地中点更近的一座桥。那么按照这个中点排序,两座桥一定分别负责一段前缀和一段后缀。枚举这样的划分。对前缀求解时,进行一遍扫描,只需要动态维护中位数,用对顶堆即可。时间复杂度 \(O(n \log n)\)

难度Medium,场切。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+5;
int n,k;ll ans;char c,d;
namespace task1{
	int a[N*2],m=0;
	void solve(){
		for(int i=1,x,y;i<=n;i++){
			cin>>c>>x>>d>>y;
			if(c==d)ans+=abs(x-y);
			else a[++m]=x,a[++m]=y;
		}
		sort(a+1,a+m+1);
		for(int i=1;i<=m;i++)ans+=abs(a[i]-a[m/2]);
		printf("%lld\n",ans+m/2);
	}
}
namespace task2{
	struct P{int l,r;}a[N];
	int m=0;ll res=1e18,pre[N],suf[N];
	bool cmp(P a,P b){return a.l+a.r<b.l+b.r;}
	void solve(){
		memset(pre,0x3f,sizeof(pre));
		memset(suf,0x3f,sizeof(suf));
		for(int i=1,x,y;i<=n;i++){
			cin>>c>>x>>d>>y;
			if(x>y)swap(x,y);
			if(c==d)ans+=y-x;
			else a[++m].l=x,a[m].r=y;
		}
		if(!m){printf("%lld\n",ans);return;}
		sort(a+1,a+m+1,cmp);
		pre[0]=suf[m+1]=0;
		priority_queue<int> Q1,Q2;
		while(!Q1.empty())Q1.pop();while(!Q2.empty())Q2.pop();
		pre[1]=a[1].r-a[1].l;
		Q1.push(a[1].l);Q2.push(-a[1].r);
		ll s1=a[1].l,s2=a[1].r;
		for(int i=2,x,tmp;i<=m;i++){
			tmp=Q1.top();
			x=a[i].l;if(x<tmp)Q1.push(x),s1+=x;else Q2.push(-x),s2+=x;
			x=a[i].r;if(x<tmp)Q1.push(x),s1+=x;else Q2.push(-x),s2+=x;
			if(Q1.size()>i){x=Q1.top();Q1.pop();s1-=x;s2+=x;Q2.push(-x);}
			if(Q2.size()>i){x=-Q2.top();Q2.pop();s2-=x;s1+=x;Q1.push(x);}
			pre[i]=s2-s1;
		}
		while(!Q1.empty())Q1.pop();while(!Q2.empty())Q2.pop();
		suf[m]=a[m].r-a[m].l;
		Q1.push(a[m].l);Q2.push(-a[m].r);
		s1=a[m].l;s2=a[m].r;
		for(int i=m-1,x,tmp;i>=1;i--){
			tmp=Q1.top();
			x=a[i].l;if(x<tmp)Q1.push(x),s1+=x;else Q2.push(-x),s2+=x;
			x=a[i].r;if(x<tmp)Q1.push(x),s1+=x;else Q2.push(-x),s2+=x;
			if(Q1.size()>m-i+1){x=Q1.top();Q1.pop();s1-=x;s2+=x;Q2.push(-x);}
			if(Q2.size()>m-i+1){x=-Q2.top();Q2.pop();s2-=x;s1+=x;Q1.push(x);}
			suf[i]=s2-s1;
		}
		for(int i=0;i<=m;i++)res=min(res,pre[i]+suf[i+1]);
		printf("%lld\n",ans+m+res);
	}
}
int main(){
	ios::sync_with_stdio(false);
	cin>>k>>n;
	if(k==1)task1::solve();
	else task2::solve();
	return 0;
}

APIO2014

2023-04-04


A 回文串

给定字符串 \(S\)。对 \(S\) 的所有回文子串,求其长度与出现次数之积的最大值。

\(|S| \le 300000\)


tag:Manacher、后缀数组 / 回文树

解法1:首先用Manacher算法求出所有的回文串。这并不难,只要在右端点每次扩展的时候记录回文串就可以了,剩下的回文串已经统计过了。

对每个回文串,我们来求它出现的次数。使用后缀数组,找到这个回文串对应的后缀在SA中对应的位置,然后向左向右两次二分,求出包含这一前缀的后缀数目。后缀数组的性质决定了这可以实现。

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

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int N=3e5+5;
int n,m,a[N<<1],lg2[N],cnt,str[N<<2][2];
int S,x[N],y[N],c[N],sa[N],rk[N],h[N],mn[N][25];
ll ans;char s[N],t[N<<1];
void SA(){
	S=300;
	for(int i=1;i<=n;i++){x[i]=s[i];++c[x[i]];}
	for(int i=2;i<=S;i++)c[i]+=c[i-1];
	for(int i=n;i>=1;i--)sa[c[x[i]]--]=i;
	for(int k=1;k<=n;k<<=1){
		int num=0;
		for(int i=n-k+1;i<=n;i++)y[++num]=i;
		for(int i=1;i<=n;i++)if(sa[i]>k)y[++num]=sa[i]-k;
		for(int i=1;i<=S;i++)c[i]=0;
		for(int i=1;i<=n;i++)c[x[i]]++;
		for(int i=2;i<=S;i++)c[i]+=c[i-1];
		for(int i=n;i>=1;i--){sa[c[x[y[i]]]--]=y[i];y[i]=0;}
		for(int i=1;i<=n;i++)swap(x[i],y[i]);
		num=1;x[sa[1]]=1;
		for(int i=2;i<=n;i++){
			if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])
				x[sa[i]]=num;
			else x[sa[i]]=++num;
		}
		if(num==n)break;S=num;
	}
}
void LCP(){
	int k=0;
	for(int i=1;i<=n;i++)rk[sa[i]]=i;
	for(int i=1;i<=n;i++){
		if(rk[i]==1)continue;
		if(k)k--;
		int j=sa[rk[i]-1];
		while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k])k++;
		h[rk[i]]=k;
	}
}
void build_ST(){
	for(int i=1;i<=n;i++)mn[i][0]=h[i];
	for(int j=1;(1<<j)<=n;j++)
		for(int i=1;i<=n-(1<<j)+1;i++)
			mn[i][j]=min(mn[i][j-1],mn[i+(1<<j-1)][j-1]);
}
int query(int l,int r){
	if(l>r)return 0;
	int i=lg2[r-l+1];int j=(1<<i);
	return min(mn[l][i],mn[r-j+1][i]);
}
void Manacher(){
	int l=1,r=0;
	for(int i=1;i<=m;++i){
		if(r>=i&&i+a[l+r-i]<=r)a[i]=a[l+r-i];
		else{
			int k=max(0,r-i);
			while(i+k<=m&&i-k>=1&&t[i+k]==t[i-k]){
				++k;++cnt;
				str[cnt][0]=(i-k+2)/2;str[cnt][1]=(i+k-1)/2;
				if(str[cnt][0]>str[cnt][1])--cnt;
			}
			a[i]=k;l=i-k+1;r=i+k-1;
		}
	}
}
ll solve(int l,int r){
	int L=0,R=0,p1=0,p2=0;
	L=2;R=rk[l];p1=rk[l]+1;
	while(L<=R){
		int mid=L+R>>1;
		if(query(mid,rk[l])>=r-l+1)p1=mid,R=mid-1;
		else L=mid+1;
	}
	L=rk[l]+1;R=n;p2=rk[l];
	while(L<=R){
		int mid=L+R>>1;
		if(query(rk[l]+1,mid)>=r-l+1)p2=mid,L=mid+1;
		else R=mid-1;
	}
	return 1ll*(p2-p1+2)*(r-l+1);
}
int main(){
	scanf("%s",s+1);
	n=strlen(s+1);m=2*n+1;t[1]='#';
	for(int i=0;(1<<i)<=n;i++)lg2[1<<i]=i;
	for(int i=1,lst=0;i<=n;i++){
		if(lg2[i])lst=lg2[i];
		else lg2[i]=lst;
	}
	for(int i=n;i>=1;--i)t[2*i]=s[i],t[2*i+1]='#';
	Manacher();SA();LCP();build_ST();
	for(int i=1;i<=cnt;i++)ans=max(ans,solve(str[i][0],str[i][1]));
	printf("%lld\n",ans);
	return 0;
}

解法2:回文树模板题。似乎不必多讲。

难度Medium,场切,但是因为数据太水。做法不是以上两种。


B 序列分割

将长为 \(n\) 的数组分 \(k\) 次,每次在某一段内部将其分成两段,得分是分出两段的元素和的乘积。\(k\) 次操作的总得分是每次得分之和。求最大得分并输出方案。

\(n \le 100000\)\(k \le 200\)\(k \lt n\)


tag:斜率优化DP

首先发现分段的顺序无关紧要,最后的得分是分出各段元素和两两积的和,也就是所有元素总和的平方,减去各段元素和的平方,再折半。所以只用求各段元素和的平方的最小值。

\(dp_{i,j}\) 表示前 \(i\) 个分为 \(j\) 段的最小值。\(dp_{i,1}=s_i^2\) 作为初始值,其中 \(s\) 是前缀和。

转移方程是

\[dp_{i,j}=\max_{1\le t \lt i} \{ dp_{t,j-1}+(s_i-s_t)^2) \} \]

然后按照斜率优化的套路写就可以了。

难度Easy+。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+5,K=205;
int n,k,lst[K][N];
ll dp[K][N],s[N],b[N],sum;
int q[N],he,ta;
int main(){
	scanf("%d%d",&n,&k);
	for(int i=1,x;i<=n;i++){
		scanf("%d",&x);
		s[i]=s[i-1]+x;sum+=x;
	}
	memset(dp,0x3f,sizeof(dp));
	for(int j=1;j<=n;j++)dp[1][j]=s[j]*s[j];
	for(int i=2;i<=k+1;i++){
		he=ta=1;q[1]=i-1;
		b[i-1]=s[i-1]*s[i-1]+dp[i-1][i-1];
		for(int j=i;j<=n;j++){
			while(he<ta&&(b[q[he+1]]-b[q[he]])<=2*s[j]*(s[q[he+1]]-s[q[he]]))++he;
			dp[i][j]=s[j]*s[j]-2*s[q[he]]*s[j]+b[q[he]];lst[i][j]=q[he];
			b[j]=s[j]*s[j]+dp[i-1][j];
			while(ta>he&&(b[j]-b[q[ta-1]])*(s[j]-s[q[ta]])>=
						 (b[j]-b[q[ta]])*(s[j]-s[q[ta-1]]))--ta;
			q[++ta]=j;
		}
	}
	printf("%lld\n",(sum*sum-dp[k+1][n])/2);
	for(int i=k,j=lst[k+1][n];i>=1;i--){
		printf("%d ",j);
		j=lst[i][j];
	}
	return 0;
}

C 连珠线

初始有一个点,用如下两种操作生成一棵树:

Append(w, v):一个新的点 \(w\) 和一个已经添加的点 \(v\) 用红边连接起来。

Insert(w, u, v):一个新的点 \(w\) 插入到用红边连起来的两个点 \(u, v\) 之间。具体过程是删去 \(u, v\) 之间红边,分别用蓝边连接 \(u, w\)\(w, v\)

给定 \(n\) 个点的最终状态。求蓝边权值之和的最大可能值。

\(n \le 200000\)


tag:换根DP

首先分析这样生成的树的蓝边分布有什么限制。显然蓝边可以按照加入时间两两配对,且配对的两条边相邻。称它们的公共点为这两条蓝边的中心。

可以把操作看成删边。考虑最后剩下某个点 \(r\) 的情形,将原树看做以 \(r\) 为根的有根树。用 \(dp_{u,0}\) 表示将以 \(u\) 为根的子树删到只剩 \(u\),且 \(u\) 不作为某组蓝边的中心时的最大值。\(dp_{u,1}\) 类似,但表示 \(u\) 作为某个中心的最大值。注意此时 \(u\) 所在的两条蓝边必定包含它向上连的边,否则删去这两条边后图出现多个连通分支,不合题意。这里 \(dp_{u,1}\) 的值不包括向上的这条边。

在此基础上,不难写出转移方程:

\[dp_{u,0}=\sum_{v是u的儿子}(\max \{ dp_{v,0},dp_{v,1}+val_{i} \}) \]

\[dp_{u,1}=dp_{u,0}-\max_{v是u的儿子} \{dp_{v,0}+val_{i}-\max \{ dp_{v,0},dp_{v,1}+val_{i} \}\} \]

其中 \(val_i\) 表示连接 \(u,v\) 的边的边权。

然后换根即可。只用预处理 \(dp_{v,0}+val_{i}-\max \{ dp_{v,0},dp_{v,1}+val_{i} \}\) 的最大值和次大值。

难度Medium,换根不熟悉。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5,INF=1<<29;
int n,dp[N][2],g[N][2],ans;
int head[N],nxt[N<<1],ver[N<<1],val[N<<1],tot;
int max(int a,int b){return a>b?a:b;}
void add(int u,int v,int w){
	ver[++tot]=v;
	val[tot]=w;
	nxt[tot]=head[u];
	head[u]=tot;
}
void dfs(int u,int fa){
	dp[u][0]=0;g[u][0]=g[u][1]=-INF;
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		if(v==fa)continue;
		dfs(v,u);
		int tmp=max(dp[v][0],dp[v][1]+val[i]);
		dp[u][0]+=tmp;
		tmp=dp[v][0]+val[i]-tmp;
		if(tmp>g[u][0])g[u][1]=g[u][0],g[u][0]=tmp;
		else if(tmp>g[u][1])g[u][1]=tmp;
	}
	dp[u][1]=g[u][0]+dp[u][0];
}
void redfs(int u,int fa,int in){
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		if(v==fa)continue;
		int t0=dp[u][0],t1=dp[u][1],t2=dp[v][0],t3=dp[v][1];
		int tmp=max(dp[v][0],dp[v][1]+val[i]);
		dp[u][0]=dp[u][0]-tmp;
		if(g[u][0]==val[i]+dp[v][0]-tmp)dp[u][1]=dp[u][0]+max(in,g[u][1]);
		else dp[u][1]=dp[u][0]+max(in,g[u][0]);
		tmp=max(dp[u][0],dp[u][1]+val[i]);
		dp[v][0]=dp[v][0]+tmp;
		dp[v][1]=dp[v][0]+max(g[v][0],val[i]+dp[u][0]-tmp);
		ans=max(ans,dp[v][0]);
		redfs(v,u,val[i]+dp[u][0]-tmp);
		dp[u][0]=t0;dp[u][1]=t1;dp[v][0]=t2;dp[v][1]=t3;
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1,u,v,w;i<n;i++){
		scanf("%d%d%d",&u,&v,&w);
		add(u,v,w);add(v,u,w);
	}
	dfs(1,-1);ans=dp[1][0];
	redfs(1,-1,-INF);
	printf("%d\n",ans);
	return 0;
}

APIO2013

2023-04-10


A 机器人

给定一个 \(w \times h\) 的带障碍网格图以及 \(n\) 个顺次编号机器人,每次将一个机器人推向某个方向知道碰到墙或障碍才会停止,如果碰到了转向器(不算障碍),会转向继续前进。在停下后若有编号相邻的,则将两个机器人合并,新编号为最小的编号与最大的编号。同一时刻只能有一个机器人移动,求最小推动次数使所有机器人合并。

$ n \le 9 $, $ w,h \le 500 $


tag:分层图DP

\(f_{i,j,l,r}\) 表示将编号 \(l\)\(r\) 的机器人合并到点 \((i,j)\) 的最小次数。

两种转移:一种是在某处将两个机器人合并,另一种是同一个机器人的移动。

前者直接枚举合并即可,后者跑最短路。

难度Hard-。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
const int N=502,INF=0x3f3f3f3f;
int T,m,n,L,R,f[N][N][10][10];
char a[N][N];bool used[N][N];pii to[N][N][4];
int dx[]={0,1,0,-1},dy[]={1,0,-1,0};
pii push(int x,int y,int d){
	if(to[x][y][d]!=pii(0,0))return to[x][y][d];
	to[x][y][d]=pii(-1,-1);
	int dd=d;
	if(a[x][y]=='C')dd=(d+1)%4;
	if(a[x][y]=='A')dd=(d+3)%4;
	int xx=x+dx[dd],yy=y+dy[dd];
	if(xx>n||xx<1||yy>m||yy<1||a[xx][yy]=='x')
		return (to[x][y][d]=pii(x,y));
	return (to[x][y][d]=push(xx,yy,dd));
}
struct Pair_Queue{
	int he,ta,t[N*N];
	pii q[N*N],c[N*N];
	inline void init(){memset(t,0,sizeof(t));}
	inline void reset(){he=1;ta=0;}
	inline bool empty(){return ta<he;}
	inline void push(int x,int y){q[++ta]=pii(x,y);}
	inline pii front(){return q[he];}
	inline void pop(){++he;}
#define dis(k) f[(k).first][(k).second][L][R]
	inline void sort(){
		int mn=INF,mx=0;
		for(int i=he,val;val=dis(q[i]),i<=ta;i++)
			c[i]=q[i],mn=min(mn,val),mx=max(mx,val),t[val]++;
		for(int i=mn+1;i<=mx;i++)t[i]+=t[i-1];
		for(int i=he;i<=ta;i++)q[t[dis(c[i])]--]=c[i];
		for(int i=mn;i<=mx;i++)t[i]=0;
	}
#undef dis
}Q1,Q2;
void bfs(int l,int r){
	Q1.reset();Q2.reset();L=l;R=r;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j)
			if(f[i][j][l][r]<INF)Q1.push(i,j);
	if(Q1.empty())return;
	memset(used,0,sizeof(used));Q1.sort();pii u;
#define dis(k) f[(k).first][(k).second][L][R]
#define use(k) used[(k).first][(k).second]
	for(int x,y;!Q1.empty()||!Q2.empty();){
		if(Q1.empty())u=Q2.front(),Q2.pop();
		else if(Q2.empty())u=Q1.front(),Q1.pop();
		else if(dis(Q1.front())<dis(Q2.front()))u=Q1.front(),Q1.pop();
		else u=Q2.front(),Q2.pop();
		x=u.first;y=u.second;used[x][y]=1;
		for(int d=0;d<4;d++){
			int xx=to[x][y][d].first,yy=to[x][y][d].second;
			if(xx<1||xx>n||yy<1||yy>m||a[xx][yy]=='x')continue;
			if(f[x][y][l][r]+1<f[xx][yy][l][r]){
				f[xx][yy][l][r]=f[x][y][l][r]+1;
				used[xx][yy]=1;Q2.push(xx,yy);
			}
		}
		while(!Q1.empty()&&use(Q1.front()))Q1.pop();
	}
}
int main(){
	memset(f,0x3f,sizeof(f));Q1.init();Q2.init();
	scanf("%d%d%d",&T,&m,&n);
	for(int i=1;i<=n;++i)scanf("%s",a[i]+1);
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j)
			if(isdigit(a[i][j])){
				int v=a[i][j]-'0';
				f[i][j][v][v]=0;
			}
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j)
			for(int d=0;d<4;++d)
				if(a[i][j]!='x')to[i][j][d]=push(i,j,d);
	for(int len=1;len<=T;++len)
		for(int l=1,r;r=l+len-1,r<=T;++l){
			for(int mid=l;mid<r;++mid)
				for(int i=1;i<=n;++i)for(int j=1;j<=m;++j)
					f[i][j][l][r]=min(f[i][j][l][r],
							f[i][j][l][mid]+f[i][j][mid+1][r]);
			bfs(l,r);
		}
	int ans=INF;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j)ans=min(ans,f[i][j][1][T]);
	printf("%d\n",ans<INF?ans:-1);
	return 0;
}

B 道路费用

给定 \(n\) 个点,\(m\) 条边的图,点,边都有权值,边权互不相同。现新加入 \(k\) 条边,权值自定,要求最大化图中某一棵最小生成树的代价。这里代价定义为:每个点到 \(1\) 号点的路径上新加入边的权值和乘以点权,再对所有点求和。求这个最大代价。

\(n \le 100000\)\(m \le 300000\)\(k \le 20\)


tag:最小生成树,缩点

注意 \(k\) 很小。

先求原图的最小生成树。然后钦定 \(k\) 条边的权值为0,再求最小生成树。这样最小生成树其余至少 \(n-k\) 条边是必选的,\(k\) 条边是可选的。对这些必选边建图缩点,图中至多剩下 \(k+1\) 个点。问题就可以转化为这样的小情况。

二进制枚举 \(k\) 条边中选用的边,对每种选择跑最小生成树。然后用上述剩下 \(k\) 条边中没有选入的边,找到树上包含它的环,更新环上边的最大权值即可。

难度Medium+。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,k,num,p[N],q[25][2],P[N],no[N];
int head[N],nxt[N<<1],ver[N<<1],tot=1;
int b[25],c[25],d[25],f[25],g[25];long long ans,a[25],sz[25];
int find(int u){return (u==P[u]?u:(P[u]=find(P[u])));}
struct edge{int u,v,w;}e[N*3];
void add(int u,int v){
	ver[++tot]=v;
	nxt[tot]=head[u];
	head[u]=tot;
}
bool operator <(const edge &a,const edge &b){return a.w<b.w;}
void dfs(int u,int fa,int t){
	no[u]=t;a[t]+=p[u];
	for(int i=head[u];i;i=nxt[i])
		if(ver[i]!=fa)dfs(ver[i],u,t);
}
void rdfs(int u){
	sz[u]=a[u];
	for(int i=head[u],v;i;i=nxt[i]){
		v=ver[i];
		if(v==f[u])continue;
		f[v]=u;d[v]=d[u]+1;
		rdfs(v);sz[u]+=sz[v];
	}
}
int main(){
	scanf("%d%d%d",&n,&m,&k);
	for(int i=1,u,v,w;i<=m;i++)scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w);
	for(int i=1;i<=k;i++)scanf("%d%d",&q[i][0],&q[i][1]);
	for(int i=1;i<=n;i++)scanf("%d",p+i);
	for(int i=1;i<=n;i++)P[i]=i;
	sort(e+1,e+m+1);
	for(int i=1,cnt=0;i<=m;i++){
		int u=e[i].u,v=e[i].v;
		int fu=find(u),fv=find(v);
		if(fu==fv)continue;
		P[fu]=fv;e[++cnt]=e[i];
	}
	for(int i=1;i<=n;i++)P[i]=i;
	for(int i=1;i<=k;i++){
		int fu=find(q[i][0]),fv=find(q[i][1]);
		if(fu==fv)continue;P[fu]=fv;
	}
	for(int i=1;i<n;i++){
		int fu=find(e[i].u),fv=find(e[i].v);
		if(fu==fv){e[++num]=e[i];continue;}
		add(e[i].u,e[i].v);add(e[i].v,e[i].u);P[fu]=fv;
	}
	for(int i=1,cnt=0;i<=n;i++)if(!no[i])dfs(i,-1,++cnt);
	for(int i=1;i<=num;i++)e[i].u=no[e[i].u],e[i].v=no[e[i].v];
	for(int i=1;i<=k;i++)q[i][0]=no[q[i][0]],q[i][1]=no[q[i][1]];
	for(int st=1;st<(1<<k);st++){
		long long res=0;
		tot=1;for(int i=1;i<=num+1;i++)head[i]=0;
		for(int i=1;i<=k;i++)b[i]=(st>>i-1)&1;
		for(int i=1;i<=num+1;i++){P[i]=i;g[i]=1e9;}
		for(int i=1;i<=k;i++)if(b[i]){
			int fu=find(q[i][0]),fv=find(q[i][1]);
			if(fu==fv){res=-1;break;}
			add(q[i][0],q[i][1]);add(q[i][1],q[i][0]);P[fu]=fv;
		}
		if(res==-1)continue;
		for(int i=1;i<=num;i++){
			int fu=find(e[i].u),fv=find(e[i].v);
			if(fu==fv){c[i]=0;continue;}
			P[fu]=fv;c[i]=1;
			add(e[i].u,e[i].v);add(e[i].v,e[i].u);
		}
		rdfs(1);
		for(int i=1;i<=num;i++)if(!c[i]){
			int u=e[i].u,v=e[i].v,w=e[i].w;
			if(d[u]<d[v])swap(u,v);
			while(d[u]>d[v]){g[u]=min(g[u],w);u=f[u];}
			while(u!=v){g[u]=min(g[u],w);g[v]=min(g[v],w);u=f[u];v=f[v];}
		}
		for(int i=1;i<=k;i++)if(b[i]){
			int u=q[i][0],v=q[i][1];
			if(d[u]>d[v])swap(u,v);
			res+=1ll*sz[v]*g[v];
		}
		ans=max(ans,res);
	}
	printf("%lld\n",ans);
	return 0;
}

C 出题人

提交答案题。

目标是造数据,输入有数字个数限制。循环超过 \(10^6\) 次算作超时。

Q1:Floyd,Heap-Dijkstra,BellmanFord 三种算法,任取两个(有序),卡掉一个而放另一个过。

Q2:求图的色数,目标算法是从小到大枚举并判断,要求放它过和卡掉它。点数 \(\gt 70\),边数 \(\gt 1500\)


tag:思维题

卡 Floyd 非常简单。

卡 BellmanFord 只要一堆自环,重边即可。

卡 Heap-Dijkstra 需要构造连续的三角形,利用负权边把它卡到指数级别。

Q2要卡掉随便构造都可以,要放过只要构造二分图。

难度Medium。


APIO2012

2023-03-29


A 派遣

\(N\) 个人的从属关系构成一棵树,第 \(i\) 个人的直接上司是 \(B_i\)\(B_i \lt i\)\(B_i=0\) 表示i为根),每个人有两个参数:薪水 \(C_i\) 和领导力 \(L_i\)。现在要确定一个管理者,选择其若干下属(直接或间接,可以不含管理者),使得选出的人得薪水总和不超过给定的预算 \(M\)。在此条件下,最大化管理者的领导力与选出的人数之积。

\(1 \le N \le 10^5\)\(1 \le M \le 10^9\)\(0 \le B_i \lt i\)\(1 \le C_i \le M\)\(1 \le L_i \le 10^9\)


tag:线段树合并 / 堆、启发式合并

先考虑固定管理者的情形。此时要最大化选出的人数,也就是要在这棵子树中从小到大依次取 \(C_i\),使得总和不超过 \(m\)

这有两种方法维护:一是用线段树合并,二分查找,时间复杂度 \(O(n \log ^2n)\)。二是用优先队列启发式合并,只维护和不超过 \(m\) 的部分,多的删掉。此时每个元素至多删除一次,所以时间复杂度 \(O(n \log n)\)

实现了第一种。

难度Easy,场切。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll,int> pli;
pli operator +(const pli &a,const pli &b){
	return make_pair(a.first+b.first,a.second+b.second);
};
const int N=1e5+5;
int n,a[N],b[N],num[N];ll m,c[N],ans;
int head[N],nxt[N],ver[N],tot;
void add(int u,int v){
	ver[++tot]=v;
	nxt[tot]=head[u];
	head[u]=tot;
}
int rt[N],segtot;
ll sum[N*20];int cnt[N*20],lc[N*20],rc[N*20];
void modify(int p,int l,int r,int x){
	if(l==r){sum[p]+=b[x];++cnt[p];return;}
	int mid=l+r>>1;
	if(x<=mid){
		if(!lc[p])lc[p]=++segtot;
		modify(lc[p],l,mid,x);
	}
	else{
		if(!rc[p])rc[p]=++segtot;
		modify(rc[p],mid+1,r,x);
	}
	sum[p]=sum[lc[p]]+sum[rc[p]];
	cnt[p]=cnt[lc[p]]+cnt[rc[p]];
}
pli query(int p,int l,int r,int L,int R){
	if(!p)return make_pair(0,0);
	if(l>=L&&r<=R)return make_pair(sum[p],cnt[p]);
	int mid=l+r>>1;pli res=make_pair(0,0);
	if(L<=mid)res=res+query(lc[p],l,mid,L,R);
	if(R>mid)res=res+query(rc[p],mid+1,r,L,R);
	return res;
}
int merge(int p,int q){
	if(!p)return q;
	if(!q)return p;
	sum[p]+=sum[q];cnt[p]+=cnt[q];
	lc[p]=merge(lc[p],lc[q]);
	rc[p]=merge(rc[p],rc[q]);
	return p;
}
void dfs(int u){
	rt[u]=++segtot;
	modify(rt[u],1,n,c[u]);
	for(int i=head[u],v;i;i=nxt[i]){
		v=ver[i];dfs(v);
		rt[u]=merge(rt[u],rt[v]);
	}
	int l=1,r=n,res=0;
	while(l<=r){
		int mid=l+r>>1;
		pli p=query(rt[u],1,n,1,mid);
		if(p.first<=m)l=mid+1,res=p.second;
		else r=mid-1;
	}
	ans=max(ans,1ll*res*a[u]);
}
int main(){
	scanf("%d%lld",&n,&m);
	for(int i=1,fa;i<=n;i++){
		scanf("%d%lld%d",&fa,c+i,a+i);b[i]=c[i];
		if(fa)add(fa,i);
	}
	sort(b+1,b+n+1);
	for(int i=1;i<=n;i++){
		int l=1,r=n,pos=0;
		while(l<=r){
			int mid=l+r>>1;
			if(b[mid]<c[i])l=mid+1;
			else r=mid-1;
			if(b[mid]==c[i])pos=mid;
		}
		c[i]=pos+num[pos];
		num[pos]++;
	}
	dfs(1);
	printf("%lld\n",ans);
	return 0;
}

B 守卫

长为 \(n\)\(01\) 数列中有 \(k\)\(1\),满足 \(m\) 个条件。每个条件形如 \(a_i\),\(b_i\),\(c_i\),含义如下:

  • \(c_i=0\) 时,区间 \([a_i,b_i]\) 中没有 \(1\)
  • \(c_i=1\) 时,区间 \([a_i,b_i]\) 中有 \(1\)

保证初始有解。求所有必定为 \(1\) 的位置。

\(1 \le n \le 10^5\)\(0 \le m \lt 10^5\)\(1 \le k \le n\)\(1 \le a_i \le b_i \le n\)


tag:贪心

首先解决简单情况:

  1. 丢掉所有条件给出为 \(0\) 的位置。
  2. 如果剩下的可能位置数与总数相同,剩下的所有位置都满足条件。
    3.如果有某个 \(c_i=1\) 的区间长为 \(1\),这个位置满足条件。
    4.如果有某两个 \(c_i=1\) 的区间有包含关系,只用考虑小的那个。

下面来考虑剩下的情形。对所有区间按左端点递增排序,则右端点也递增。

首先可以贪心求出一组解(不考虑 \(k\)),这只要从左到右扫描所有区间,如果某个区间还没有 \(1\),在其右端点放 \(1\)。同时可以求出满足前 \(i\) 个区间所需要的 \(1\) 的数目的最小值 \(f_i\)。类似的,可以求出满足后 \(i\) 个区间所需要的 \(1\) 的数目的最小值 \(g_i\)

“某个位置必须是 \(1\)”等价于“某个位置是 \(0\) 时无解”。只用考虑那些满足 \(f_i = f_{i-1} + 1\) 的区间 \([a_i,b_i]\) 的右端点,记为 \(x\)。则满足前 \(i\) 个区间的条件需要 \(f_i\)\(1\)

再考虑所有完全在 \(x-1\) 右侧的区间,设为后 \(p\) 个。假设 \(x\) 不是 \(1\),所以满足前 \(i\) 个区间时不会满足后 \(p\) 个区间中的任何一个。那么无解的充分必要条件就是 \(f_i + g_p \gt k\)

\(p\) 可以二分求。时间复杂度 \(O(n \log n)\)

难度Medium+。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,k,m,d[N],t,h[N],lst[N],nxt[N],cnt,st[N],top,f[N],g[N],flag;
struct range{int l,r;}p[N];
bool operator <(const range &a,const range &b){
	return a.l!=b.l?a.l<b.l:a.r<b.r;
}
int main(){
	scanf("%d%d%d",&n,&k,&m);
	for(int i=1,a,b,c;i<=m;i++){
		scanf("%d%d%d",&a,&b,&c);
		if(c==0)++d[a],--d[b+1];
		else{++t;p[t].l=a;p[t].r=b;}
	}
	for(int i=1;i<=n;i++){
		d[i+1]+=d[i];
		if(!d[i]){lst[i]=nxt[i]=++cnt;h[cnt]=i;}
	}
	if(k==cnt){
		for(int i=1;i<=cnt;i++)printf("%d\n",h[i]);
		printf("\n");
		return 0;
	}
	for(int i=1;i<=n;i++)if(!lst[i])lst[i]=lst[i-1];
	for(int i=n;i>=1;i--)if(!nxt[i])nxt[i]=nxt[i+1];
	for(int i=1;i<=t;i++)p[i].l=nxt[p[i].l],p[i].r=lst[p[i].r];
	sort(p+1,p+t+1);top=0;
	for(int i=1;i<=t;i++){
		if(p[i].l>p[i].r)continue;
		while(top&&p[st[top]].r>=p[i].r)--top;
		st[++top]=i;
	}t=top;
	for(int i=1;i<=t;i++)p[i]=p[st[i]];
	int mx=0,mn=1e9;
	for(int i=1;i<=t;i++){
		if(p[i].l>mx)f[i]=f[i-1]+1,mx=p[i].r;
		else f[i]=f[i-1];
	}
	for(int i=t;i>=1;i--){
		if(p[i].r<mn)g[i]=g[i+1]+1,mn=p[i].l;
		else g[i]=g[i+1];
	}
	for(int i=1;i<=t;i++){
		if(p[i].l==p[i].r){flag=1;printf("%d\n",h[p[i].l]);continue;}
		if(f[i]!=f[i-1]+1)continue;
		int pos=t+1,l=i+1,r=t;
		while(l<=r){
			int mid=l+r>>1;
			if(p[mid].l>p[i].r-1)pos=mid,r=mid-1;
			else l=mid+1;
		}
		if(f[i]+g[pos]>k){flag=1;printf("%d\n",h[p[i].r]);}
	}
	if(!flag)printf("-1\n");
	return 0;
}

C 苦无

\(W \times H\) 的表格中,有 \(n\) 个质点从某方格中心同时开始运动。没有两个质点初始时在同一方格中。所有质点以同样的速度做直线运动,方向为上、下、左、右之一。如果两个或多个质点在同一时刻位于同一位置,则这些质点相撞并消失。求被质点经过的格子数。

这里每个质点用三个参数 \(x\)\(y\)\(d\) 来描述,表示其开始运动的位置是从左往右的 \(x\) 列、从上往下的第 \(y\) 行的方格的中心。运动的方向由 \(d\) 表示,分别为:

  • \(d = 0\),表示向右;
  • \(d = 1\),表示向上;
  • \(d = 2\),表示向左;
  • \(d = 3\),表示向下。

\(1 \le n \le 10^5\)\(1 \le w \le 10^9\)\(1 \le h \le 10^9\)\(1 \le x \le W\)\(1 \le y \le H\)\(0 \le d \le 3\)


tag:模拟、堆、扫描线

只用求出每个质点的运动路程,然后看作宽为 \(1\) 的矩形做扫描线即可。

求运动路程,就只需要求相撞的情况。相撞质点的方向有 \(6\) 种可能,如同一行,左边的向右,右边的向左;再如同一条左上—右下对角线,左边的向下,右边的向左;等等。

在每一种可能中,最先相撞的一定是两个相邻的点。一旦遇到两个相撞,就要删去这两个点,考虑新产生的相邻对,于是使用链表。

按照 \(6\) 种情形,\(4\) 个方向排序,排序后构建链表。然后将所有相撞按时间放入一个堆中,每次取出某个时刻所有的相撞,判断是否存在(可能已经撞过)。如果是存在的相撞,就更新链表和相撞。

细节:

  1. 排序的时候要先按照方向分开。
  2. 可以假设质点每秒运动半个方格边长,这样所有的时间都是整数。

难度Medium+。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+5,INF=1<<30;
int n,W,H,X[N<<1];ll ans;
struct P{int no,x,y,d,nxt[6],lst[6],t,ex;}a[N];
int fir[]={0,3,0,3,0,1},sec[]={2,1,1,2,3,2},dx[]={1,0,-1,0},dy[]={0,-1,0,1};
bool cmp(P a,P b){return a.no<b.no;}
bool cmp1(P a,P b){
	if((a.d==0||a.d==2)&&(b.d==1||b.d==3))return true;
	if((b.d==0||b.d==2)&&(a.d==1||a.d==3))return false;
	return a.y!=b.y?a.y<b.y:a.x<b.x;
}
bool cmp2(P a,P b){
	if((a.d==1||a.d==3)&&(b.d==0||b.d==2))return true;
	if((b.d==1||b.d==3)&&(a.d==0||a.d==2))return false;
	return a.x!=b.x?a.x<b.x:a.y<b.y;
}
bool cmp3(P a,P b){
	if((a.d==0||a.d==1)&&(b.d==2||b.d==3))return true;
	if((b.d==0||b.d==1)&&(a.d==2||a.d==3))return false;
	return a.x-a.y!=b.x-b.y?a.x-a.y<b.x-b.y:a.x<b.x;
}
bool cmp4(P a,P b){
	if((a.d==0||a.d==3)&&(b.d==1||b.d==2))return true;
	if((b.d==0||b.d==3)&&(a.d==1||a.d==2))return false;
	return a.x+a.y!=b.x+b.y?a.x+a.y<b.x+b.y:a.x<b.x;
}
struct hit{int t,p,q;};
bool operator <(const hit &a,const hit &b){return a.t>b.t;}
priority_queue<hit> Q;
struct ScanLine{
	ll l,r,h;int op;
	bool operator <(const ScanLine &b){
		return h<b.h;
	}
}line[N<<1];
struct SegmentTree{
	int ls[N<<3],rs[N<<3],sum[N<<3];ll len[N<<3];
	#define lc p<<1
	#define rc p<<1|1
	void push_up(int p){
		if(sum[p])len[p]=X[rs[p]+1]-X[ls[p]];
		else len[p]=len[lc]+len[rc];
	}
	void build(int p,int l,int r){
		ls[p]=l;rs[p]=r;len[p]=sum[p]=0;
		if(l==r)return;
		build(lc,l,l+r>>1);
		build(rc,(l+r>>1)+1,r);
	}
	void modify(int p,int L,int R,int c){
		if(X[rs[p]+1]<=L||R<=X[ls[p]])return;
		if(L<=X[ls[p]]&&X[rs[p]+1]<=R){sum[p]+=c;push_up(p);return;}
		modify(lc,L,R,c);modify(rc,L,R,c);
		push_up(p);
	}
}seg;
int main(){
	//freopen("kunai.in","r",stdin);
	//freopen("kunai.out","w",stdout);
	scanf("%d%d%d",&W,&H,&n);
	for(int i=1;i<=n;i++){
		scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].d);
		a[i].ex=1;a[i].no=i;
		for(int k=0;k<6;k++)a[i].nxt[k]=a[i].lst[k]=-1;
		switch(a[i].d){
			case 0:a[i].t=2*(W-a[i].x);break;
			case 1:a[i].t=2*(a[i].y-1);break;
			case 2:a[i].t=2*(a[i].x-1);break;
			case 3:a[i].t=2*(H-a[i].y);break;
		}
	}
	a[n+1].d=2;a[0].d=0;
	sort(a+1,a+n+1,cmp1);
	for(int i=1;i<n;i++){
		if(a[i].d==1||a[i].d==3)break;
		if(a[i].y==a[i+1].y)a[i].nxt[0]=a[i+1].no,a[i+1].lst[0]=a[i].no;
	}
	sort(a+1,a+n+1,cmp2);
	for(int i=1;i<n;i++){
		if(a[i].d==0||a[i].d==2)break;
		if(a[i].x==a[i+1].x)a[i].nxt[1]=a[i+1].no,a[i+1].lst[1]=a[i].no;
	}
	sort(a+1,a+n+1,cmp3);
	for(int i=0,op=2;i<n;i++){
		if((a[i].d==0||a[i].d==1)&&(a[i+1].d==2||a[i+1].d==3)){op=3;continue;}
		if(i!=0&&a[i].x-a[i].y==a[i+1].x-a[i+1].y)
			a[i].nxt[op]=a[i+1].no,a[i+1].lst[op]=a[i].no;
	}
	sort(a+1,a+n+1,cmp4);
	for(int i=0,op=4;i<n;i++){
		if((a[i].d==0||a[i].d==3)&&(a[i+1].d==1||a[i+1].d==2)){op=5;continue;}
		if(i!=0&&a[i].x+a[i].y==a[i+1].x+a[i+1].y)
			a[i].nxt[op]=a[i+1].no,a[i+1].lst[op]=a[i].no;
	}
	sort(a+1,a+n+1,cmp);
	for(int k=0;k<6;k++)
		for(int i=1;i<=n;i++)
			if(a[i].nxt[k]!=-1&&a[i].d==fir[k]&&a[a[i].nxt[k]].d==sec[k])
				Q.push({abs(a[i].x-a[a[i].nxt[k]].x)+abs(a[i].y-a[a[i].nxt[k]].y),i,a[i].nxt[k]});
	while(!Q.empty()){
		int t0=Q.top().t;
		while(!Q.empty()&&Q.top().t==t0){
			hit tmp=Q.top();int i=tmp.p,j=tmp.q;Q.pop();
			if(!a[i].ex&&a[i].t!=t0||!a[j].ex&&a[j].t!=t0)continue;
			if(a[i].ex){
				for(int k=0;k<6;k++){
					int LS=a[i].lst[k],NX=a[i].nxt[k];
					if(LS!=-1)a[LS].nxt[k]=NX;
					if(NX!=-1)a[NX].lst[k]=LS;
					if(LS!=-1&&NX!=-1&&a[LS].d==fir[k]&&a[NX].d==sec[k])
						Q.push({abs(a[LS].x-a[NX].x)+abs(a[LS].y-a[NX].y),LS,NX});
				}
			}
			if(a[j].ex){
				for(int k=0;k<6;k++){
					int LS=a[j].lst[k],NX=a[j].nxt[k];
					if(LS!=-1)a[LS].nxt[k]=NX;
					if(NX!=-1)a[NX].lst[k]=LS;
					if(LS!=-1&&NX!=-1&&a[LS].d==fir[k]&&a[NX].d==sec[k])
						Q.push({abs(a[LS].x-a[NX].x)+abs(a[LS].y-a[NX].y),LS,NX});
				}
			}
			a[i].t=a[j].t=t0;a[i].ex=a[j].ex=0;
		}
	}
	for(int i=1;i<=n;i++){
		int dist=a[i].t,X1,Y1,X2,Y2;
		if(dist%2==0)dist/=2;
		switch(a[i].d){
			case 0:{X1=a[i].x;X2=a[i].x+dist+1;Y1=a[i].y;Y2=a[i].y+1;break;}
			case 1:{X1=a[i].x;X2=a[i].x+1;Y1=a[i].y-dist;Y2=a[i].y+1;break;}
			case 2:{X1=a[i].x-dist;X2=a[i].x+1;Y1=a[i].y;Y2=a[i].y+1;break;}
			case 3:{X1=a[i].x;X2=a[i].x+1;Y1=a[i].y;Y2=a[i].y+dist+1;break;}
		}
		X[2*i-1]=X1;X[2*i]=X2;
		line[2*i-1]={X1,X2,Y1,1};line[2*i]={X1,X2,Y2,-1};
	}
	sort(line+1,line+2*n+1);
	sort(X+1,X+2*n+1);
	int tot=unique(X+1,X+2*n+1)-X-1;
	seg.build(1,1,tot-1);
	for(int i=1;i<2*n;i++){
		seg.modify(1,line[i].l,line[i].r,line[i].op);
		ans+=seg.len[1]*(line[i+1].h-line[i].h);
	}
	printf("%lld\n",ans);
	return 0;
}
posted @ 2023-05-30 19:20  by_chance  阅读(45)  评论(0编辑  收藏  举报