正睿2019CSP冲刺 选做

更新中...

day1-序列

题目链接

枚举最终序列第一个位置上数的奇偶性,这样最终序列里每个位置上数的奇偶性就都确定了。特别地,如果原序列长度为奇数,则第一个位置上数的奇偶性不用枚举,看哪种数出现的多就是哪种数。

确定了最终序列里每个位置数的奇偶性后,发现奇数和偶数的移动方式互不影响,可以分别计算代价。

问题转化为,把原序列里一些位置上的数,移动到最终序列的一些位置上,使得移动距离之和最小。在此基础上,使得最终序列的字典序最小。

如果不考虑字典序,那么一种一定合法的移动方案是:让所有数相对位置不变。即:从左往右数起,第一个要移动的数,移到第一个坑;第二个数移到第二个坑,以此类推。显然,这样做移动距离之和是最小的。我们称这种方案为初始移动方案。当然,可能存在移动距离和它相等的方案,但是字典序更小。于是我们要问:什么样的移动方式,能使移动距离和初始移动方案一样小?

我们把原序列里要移动的数分为三类:

  1. 在初始移动方案中向左移的数。即:它在最终序列里的位置,比在原序列里的位置小。
  2. 在初始移动方案中位置不变的数。
  3. 在初始移动方案中向右移的数。即:它在最终序列里的位置,比在原序列里的位置大。

我们发现,任意一个移动方案,它的移动距离和初始移动方案一样小,当且仅当在这种移动方案下,每个要移动的数的类别和初始移动方案下相同

于是,我们可以把所有要移动的数,按在初始移动方案下的类别分段,每一段内的数类别相同。那么根据上面的分析,不同的段之间不会有移动,也就是说,每一段是独立的。

考虑在当前段内,如何在让移动距离不变的前提下,使字典序最小。如果当前段里是第二类数,显然每个数的位置都不能改变,直接按初始移动方案摆放即可。如果当前段里是第一类数,我们将它们按数值从大到小排序,依次把每个数都尽可能靠后放(前提是它必须是“左移”的)。如果当前段里是第二类数,我们将它们按数值从小到大排序,依次把每个数都尽可能靠前放(前提是它必须是“右移”的)。具体实现中,我们可以用set来维护尚未占用的位置,二分出当前数前面/后面第一个未被占用的位置,并从set里删除即可。

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

参考代码(片段):

const int MAXN=1e5;
int n,a[MAXN+5],vals[MAXN+5],flag[MAXN+5],res1[MAXN+5],res2[MAXN+5];
ll solve(int st_odd,int* res){
	static int pos[MAXN+5];
	for(int i=1,odd=st_odd,even=3-st_odd;i<=n;++i){
		pos[a[i]]=i;//pos[x]: x这个值在原序列里的出现位置
		if(vals[a[i]]&1){
			res[odd]=a[i];
			odd+=2;
		}
		else{
			res[even]=a[i];
			even+=2;
		}
	}
	#define type(i) ((pos[res[i]]<(i))?1:(pos[res[i]]==(i)?0:-1))
	for(int cur=1;cur<=2;++cur){//当前要填:奇数位/偶数位
		for(int i=cur;i<=n;i+=2){
			int j=i;
			set<int>s;
			vector<int>v;
			s.insert(i);
			v.pb(res[i]);
			while(j+2<=n&&type(j+2)==type(i)){
				j+=2;
				s.insert(j);
				v.pb(res[j]);
			}
			sort(v.begin(),v.end());
			if(type(i)==1){
				//原 --右移--> 新
				for(int k=0;k<SZ(v);++k){
					int p=*s.lob(pos[v[k]]);
					s.erase(p);
					res[p]=v[k];
				}
			}
			else if(type(i)==-1){
				//原 --左移--> 新
				for(int k=SZ(v)-1;k>=0;--k){
					int p=*(--s.upb(pos[v[k]]));
					s.erase(p);
					res[p]=v[k];
				}
			}
			i=j;
		}
	}
	#undef type
	ll ans=0;
	for(int i=1;i<=n;++i)ans+=abs(i-pos[res[i]]);
	return ans;
}
int main() {
	cin>>n;
	int cnt_odd=0;
	for(int i=1;i<=n;++i)cin>>a[i],vals[i]=a[i],cnt_odd+=a[i]&1;
	sort(vals+1,vals+n+1);
	for(int i=1;i<=n;++i){
		a[i]=lob(vals+1,vals+n+1,a[i])-vals;
		a[i]+=(flag[a[i]]++);
	}
	if(n&1){
		solve(cnt_odd>n-cnt_odd?1:2,res1);
		for(int i=1;i<=n;++i)cout<<vals[res1[i]]<<" \n"[i==n];
	}
	else{
		ll v1=solve(1,res1);
		ll v2=solve(2,res2);
		if(v1<v2||(v1==v2&&res1[1]<res2[1])){
			for(int i=1;i<=n;++i)cout<<vals[res1[i]]<<" \n"[i==n];
		}
		else{
			for(int i=1;i<=n;++i)cout<<vals[res2[i]]<<" \n"[i==n];
		}
	}
	return 0;
}

day1-灯泡

题目链接

建出一张图。如果第\(i\)个灯泡和第\(i+1\)个灯泡同时亮着,我们就在\(i\)\(i+1\)两个点之间连一条边。则极长亮灯区间数(答案)就是这张图里的连通块数。

显然,每个连通块都是一条链。而链是特殊的树,它具有树的性质:一棵树的 点数-边数\(=1\),一个森林的连通块数=总点数-总边数。问题转化为,对亮着的点,分别维护它们的总点数和总边数。

总点数是很好维护的。考虑如何维护总边数。

设置一个阈值\(B\)

对于每种颜色,我们根据它在序列里出现的次数,分为次数\(\leq B\)的颜色(小颜色)和次数\(>B\)的颜色(大颜色)。

如果修改小颜色,我们直接暴力枚举这种颜色的每一次出现,根据它两边的点的状态,更新总边数。复杂度\(O(B)\)

如果修改大颜色,则与这个大颜色的点相连的边有两种:

  • 和其他大颜色相连。其他大颜色的数量只有\(\frac{n}{B}\)个,暴力枚举每个大颜色,更新总边数。
  • 和小颜色相连。我们对每个大颜色,维护一个标记。在修改小颜色时,枚举所有与它相连的大颜色,更新这种大颜色的标记,表示如果这种大颜色被修改,会对总边数造成多大的影响。这样,在修改大颜色时,这部分的贡献就等于该颜色的标记。

时间复杂度\(O(q(B+\frac{n}{B}))\)

参考代码:

const int MAXN=2e5,B=500;
int n,q,m,a[MAXN+5],app[MAXN+5],id[MAXN+5],cnt[B+5][B+5],mem[B+5],E,V;
bool st[MAXN+5];
vector<int>G[MAXN+5],big;
int main() {
	cin>>n>>q>>m;
	for(int i=1;i<=n;++i){
		cin>>a[i];
		if(a[i]==a[i-1]){i--,n--;continue;}
		app[a[i]]++;
	}
	for(int i=1;i<=n;++i){
		if(i!=1)G[a[i]].pb(a[i-1]);
		if(i!=n)G[a[i]].pb(a[i+1]);
	}
	for(int i=1;i<=m;++i){
		if((int)G[i].size()>B){
			id[i]=big.size();
			big.pb(i);
		}
	}
	for(int i=0;i<(int)big.size();++i){
		int u=big[i];
		for(int j=0;j<(int)G[u].size();++j){
			if((int)G[G[u][j]].size()>B){
				cnt[id[u]][id[G[u][j]]]++;
			}
		}
	}
	while(q--){
		cin>>u;
		if(st[u])V-=app[u];else V+=app[u];
		if((int)G[u].size()<=B){
			for(int i=0;i<(int)G[u].size();++i){
				int v=G[u][i];
				if(st[u]&&st[v])E--;
				else if(!st[u]&&st[v])E++;
				if((int)G[v].size()>B){
					if(st[u])mem[id[v]]--;
					else mem[id[v]]++;
				}
			}
		}else{
			if(st[u])E-=mem[id[u]];else E+=mem[id[u]];
			for(int i=0;i<(int)big.size();++i){
				int v=big[i];
				if(v==u)continue;
				if(st[u]&&st[v])E-=cnt[id[u]][id[v]];
				else if(!st[u]&&st[v])E+=cnt[id[u]][id[v]];
			}
		}
		st[u]^=1;
		cout<<V-E<<endl;
	}
	return 0;
}

day1-比赛

题目链接

我们定义,\(f(n,k)\)表示\(n\)个人中,存在一个大小为\(k\)的合法集合的概率。可以形式化地定义为:\(f(n,k)=\sum_{|s|=k}\prod_{i\in s}p^{\operatorname{cntGreater}(i)}(1-p)^{\operatorname{cntLess}(i)}\)。其中\(\operatorname{cntGreater}(i)\)表示不在集合里的选手中编号大于\(i\)的选手数量,\(\operatorname{cntLess}(i)\)表示不在集合里的选手中编号小于\(i\)的选手数量。

我们可以DP求\(f\)。考虑转移。考虑\(f(n+1,k)\),我们可以从\(f(n,k)\)\(f(n,k-1)\)两个地方转移,分别对应了第\(n+1\)个人 不在/在 颁奖集合里。如果\(n+1\)不在颁奖集合里,那么他要输给前面的\(k\)个在颁奖集合里的人;如果\(n+1\)在颁奖集合里,那么他要赢得前面的\(n-k+1\)个不在颁奖集合里的人。于是可以得到对应的转移系数:

\[f(n+1,k)=f(n,k)\cdot p^{k}+f(n,k-1)\cdot (1-p)^{n-k+1}\tag{1} \]

大力实现这个DP,时间复杂度是\(O(n^2)\)的。考虑优化。

我们思考另一种转移。我们还是让\(f(n+1,k)\)\(f(n,k)\)\(f(n,k-1)\)转移过来,但不是新增一个编号最大的人,而是新增一个编号最小的人!即,之前我们让\(n+1\)做出转移,现在我们让\(1\)来做转移。如果\(1\)不在颁奖集合里,那么他要输给后面的\(k\)个在颁奖集合里的人;如果\(1\)在颁奖集合里,那么他要赢得后面的\(n-k+1\)个不在颁奖集合里的人。于是可以写出另一种转移式:

\[f(n+1,k)=f(n,k)\cdot (1-p)^k+f(n,k-1)\cdot p^{n-k+1}\tag{2} \]

\((1)\), \((2)\)两个式子联立。可得:

\[f(n,k)\cdot p^{k}+f(n,k-1)\cdot (1-p)^{n-k+1}=f(n,k)\cdot (1-p)^k+f(n,k-1)\cdot p^{n-k+1} \]

移项得:

\[f(n,k)\cdot (p^k-(1-p)^k)=f(n,k-1)\cdot (p^{n-k+1}-(1-p)^{n-k+1}) \]

\(p\neq \frac{1}{2}\)时,我们就相当于得到了对于同一个\(n\),不同的\(k\)\(f(n,k)\)的递推式。直接\(O(n)\)递推一遍,就能求出所有\(k\)的答案了。

\(p=\frac{1}{2}\)时,上述的式子不再能用于递推。我们要特判这种情况,并对这种情况单独想一个解法(有点二合一的意思)。幸运的是,这种情况的解法其实非常简单。因为所有人,无论编号,获胜的概率都一样。对于同一个\(k\),我们选出任意\(k\)个人作为颁奖集合都是等价的。所以直接用选出一个合法集合的概率,乘以\(n\choose k\)即可。即:

\[f(n,k)={n\choose k}\frac{1}{2^{k(n-k)}} \]

时间复杂度\(O(n)\)\(O(n\log n)\)。其中\(\log\)是如果你不做预处理,直接一边递推一边快速幂带来的。

参考代码(片段):

const int MAXN=1e6,MOD=998244353;
inline int pow_mod(int x,int i){
	int y=1;
	while(i){
		if(i&1)y=(ll)y*x%MOD;
		x=(ll)x*x%MOD;
		i>>=1;
	}
	return y;
}
inline int mod(int x){return x<MOD?(x<0?x+MOD:x):x-MOD;}
int n,p,q,f[MAXN+5],F[MAXN+5],fac[MAXN+5],invf[MAXN+5],ans;
int main() {
	cin>>n>>p>>q;
	p=(ll)p*pow_mod(q,MOD-2)%MOD;q=mod(1-p);
	if(p==q){
		fac[0]=1;for(int i=1;i<=n;++i)fac[i]=(ll)fac[i-1]*i%MOD;
		invf[n]=pow_mod(fac[n],MOD-2);
		for(int i=n-1;i>=0;--i)invf[i]=(ll)invf[i+1]*(i+1)%MOD;
		assert(invf[0]==1);
		f[1]=1;
		for(int i=1;i<n;++i){
			ans=mod(ans+(ll)f[i]*fac[n]%MOD*invf[i]%MOD*invf[n-i]%MOD*pow_mod(pow_mod(2,(ll)i*(n-i)%(MOD-1)),MOD-2)%MOD);
			f[i+1]=mod((ll)f[i]*f[i]%MOD+2);
		}
		cout<<ans<<endl;
		return 0;
	}
	F[1]=(ll)mod(pow_mod(p,n)-pow_mod(q,n))*pow_mod(mod(p-q),MOD-2)%MOD;
	f[1]=1;ans=F[1];
	for(int i=2;i<n;++i){
		f[i]=mod((ll)f[i-1]*f[i-1]%MOD+2);
		F[i]=(ll)F[i-1]*mod(pow_mod(p,n-i+1)-pow_mod(q,n-i+1))%MOD*pow_mod(mod(pow_mod(p,i)-pow_mod(q,i)),MOD-2)%MOD;
		ans=mod(ans+(ll)f[i]*F[i]%MOD);
	}
	cout<<ans<<endl;
	return 0;
}//4 2 6

day2-石子

题目链接

第一堆石子被取走的期望时间,等于在第一堆石子之前被取走的堆数的期望,加\(1\)

根据期望的线性性,第一堆石子之前被取走的堆数的期望,可以拆成每一堆石子的期望之和。而每一堆石子对总堆数的贡献要么是\(0\),要么是\(1\),因此它的期望在数值上就等于它在第一堆之前被取走的概率。我们设第\(i\)堆石子(\(i\geq2\))在第一堆之前被取走的概率为\(P_i\)。则答案等于\(\sum_{i=2}^{n}P_i+1\)

\(i\)堆石子在第一堆石子之前被取走,这个事件和其它石子被取的情况是无关的。也就是说,只要考虑这两堆的情况。所以,\(P_i=\frac{a_i}{a_1+a_i}\)

答案就是\(\sum_{i=2}^{n}\frac{a_i}{a_1+a_i}+1\),直接计算即可。

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

参考代码(片段):

int n;
long double a[100005];
int main() {
	cin>>n;
	for(int i=1;i<=n;++i)cin>>a[i];
	long double ans=0;
	for(int i=2;i<=n;++i)ans+=a[i]/(a[i]+a[1]);
	ans+=1;
	cout<<setiosflags(ios::fixed)<<setprecision(233)<<ans<<endl;
	return 0;
}

day2-内存

题目链接

暴力的做法是,我们枚举\(x\in[0,m)\),用当前\(x\)能取到的最优解\(f(x)\)更新答案。

对于给定的\(x\),如何求\(f(x)\)?我们可以二分答案\(\text{mid}\),贪心地从左往右扫,每当当前段的和\(>\text{mid}\),就令最后一个数自成一个新的段。若总段数\(\leq k\),说明当前\(\text{mid}\)可行,否则\(\text{mid}\)需要变大。

上述做法总时间复杂度\(O(mn\log n)\)

考虑优化。我们按随机顺序访问所有\(x\)。维护一个全局的最优答案\(\text{ans}\)。访问某个\(x\)时,先令\(\text{mid}=\text{ans}\),做一次check,如果check未通过,说明当前\(x\)\(f(x)\)一定大于\(\text{ans}\),可以直接跳到下一个\(x\)。否则我们和前面一样老老实实二分\(f(x)\),更新答案。

考虑这么做的时间复杂度。首先,因为每个\(x\)会先check一次\(\text{mid}=\text{ans}\),所以时间复杂度至少是\(O(mn)\)。如果这第一次check就未通过,我们就不需要继续进行二分了。考虑第一次check通过的概率,相当于当前\(x\)\(f(x)\),是我们访问过的所有\(x\)前缀最小值。我们以随机顺序访问\(x\),第\(i\)\(f(x)\)是前缀最小值的概率为\(\frac{1}{i}\),所以每个\(x\)第一次check能通过的期望次数之和是\(O(\sum_{i\leq m}\frac{1}{i})=O(\ln m)\)次。因此,总时间复杂度为\(O(nm+\ln m\cdot n\log n)\)

参考代码(片段):

int n,m,K,a[100005],X[1005];
inline int mod(int x){return x<m?x:x-m;}
bool check(int mid,int x){
	int cur=0,cnt=0,ok=1;
	for(int i=1;i<=n;++i){
		int t=mod(a[i]+x);
		if(t>mid){ok=0;break;}
		if(cur+t>mid){
			++cnt;
			cur=0;
		}
		cur+=t;
	}
	if(cur)++cnt;
	if(cnt>K||!ok)return 0;
	return 1;
}
int main() {
	srand((ull)time(0)^(ull)(new char));
	cin>>n>>m>>K;
	for(int i=1;i<=n;++i)cin>>a[i];
	
	for(int i=1;i<=m;++i)X[i]=i-1;
	random_shuffle(X+1,X+m+1);
	
	int ans=n*m;
	for(int t=1;t<=m;++t){
		int x=X[t];
		if(!check(ans-1,x))continue;
		int l=0,r=ans;
		while(l<r){
			int mid=(l+r)>>1;
			if(check(mid,x))r=mid;
			else l=mid+1;
		}
		//cout<<l<<endl;
		ans=min(ans,l);
	}
	cout<<ans<<endl;
	return 0;
}

day2-子集

题目链接

首先,选出来的数一定都是\(n\)的约数,否则\(\operatorname{lcm}\)不会等于\(n\)。具体来说:如果把\(n\)分解质因数后\(n=p_1^{c_1}p_2^{c_2}\cdots p_{k}^{c_k}\),则对于每个质因数\(p_i\),集合中一定有至少一个数中\(p_i\)的次数为\(c_i\),也一定有至少一个数中\(p_i\)的次数为\(0\)

考虑容斥原理。如果\(n\)只有一个质因数,即\(n=p_1^{c_1}\),则用总方案数(情况二),减去不存在\(p_1\)的次数为\(c_1\)的方案数(情况二),减去不存在\(p_1\)的次数为\(0\)的方案数(情况三),再加上两者都不存在的方案数(情况四)。

\(n\)有多个质因子时,不妨记\(n\)\(k(n)\)个质因子。我们在\(O(4^{k(n)})\)的时间里枚举每个质因子属于那种情况,乘上对应的容斥系数,然后加入答案中。但是这个时间复杂度不足以通过本题。考虑优化。

对于一个在\(n\)中次数为\(c_i\)的质因数,如果它是情况一,那么它有\(c_i+1\)种选择。如果它是情况二或者情况三,那么它有\(c_i\)种选择。如果它是情况四,那么它有\(c_i-1\)种选择。注意到,情况二和情况三的方案数是相同的!所以我们不枚举是四种情况中的哪一种,而是枚举是三种方案数中的哪一种。如果方案数是\(c_i+1\)\(c_i-1\),则容斥系数不变,否则容斥系数乘以\(-2\)。时间复杂度\(O(3^{k(n)}k(n))\),可以通过本题。

以上就是本题的主要思路。还有一个小问题:如何对\(n\)分解质因数呢?本题中\(n\)高达\(10^{18}\),传统的\(O(\sqrt{n})\)方法无法胜任。当然,如果你会PollardRho算法,那么你可以跳过此部分。但是注意到我们其实只需要知道每个质因子的次数,而不用知道每个质因子具体是什么。所以不需要使用PollardRho算法。我们先把\(n\)\(\leq\sqrt[3]{n}\)的质因子全部筛掉。剩下的数只有几种情况:

  • 剩下的数是\(1\)。也就是说\(n\)没有大于\(\sqrt[3]{n}\)的质因子。
  • 剩下的数是质数。这个大质数判定可以用MillerRabin算法实现。
  • 剩下的数是完全平方数,那么它一定是某个质数的平方。
  • 如果不是以上三种情况,说明剩下的数是两个次数均为\(1\)的不同质因子相乘。

这样,总时间复杂度就是\(O(\sqrt[3]{n}+\log n+3^{k(n)})\)

参考代码(片段):

const ll MOD=998244353LL;
inline ll mul(ll x,ll y,ll M=MOD){return (__int128)x*y%M;}//迫真快速乘 
inline ll pow_mod(ll x,ll i,ll M=MOD){
	ll y=1;
	while(i) {
		if(i&1) y=mul(y,x,M);
		x=mul(x,x,M);
		i>>=1;
	}
	return y;
}
namespace MR{
inline bool check(ll x,int a,ll d){
	if(!(x&1LL))return false;/*x!=2 且 x为偶数*/
	while(!(d&1LL))d>>=1;
	ll t=::pow_mod(a,d,x);
	if(t==1 || t==x-1)return true;
	while(t!=1 && d!=x-1){
		t=::mul(t,t,x);
		d<<=1;
		if(t==x-1)return true;
	}
	return false;
}
const int a[10]={2,3,5,7,11,13,17,19,23,29};
inline bool is_prime(ll x){
	if(x==1)return false;
	for(int i=0;i<10;++i)if(x==a[i])return true;
	for(int i=0;i<10;++i){
		if(!check(x,a[i],x-1))return false;
	}
	return true;
}
}//namespace MR
bool is_sqr(ll x){ll t=sqrt(x);return t*t==x;}
int a[100],cnt,pw[100];
int main() {
	ll n;cin>>n;
	for(ll i=2;i*i<=n&&i<=1000000;++i){
		int e=0;
		while(n%i==0)e++,n/=i;
		if(e)a[++cnt]=e;
	}
	if(n!=1){
		if(MR::is_prime(n))a[++cnt]=1;
		else if(is_sqr(n))a[++cnt]=2;
		else a[++cnt]=1,a[++cnt]=1;
	}
	pw[0]=1;for(int i=1;i<=cnt;++i)pw[i]=pw[i-1]*3;
	int ans=0;
	for(int i=0;i<pw[cnt];++i){
		ll x=1;int y=1;
		for(int j=1;j<=cnt;++j){
			int e=i/pw[j-1]%3;
			x=x*(a[j]+1-e);
			if(e==1)y*=-2;
		}
		if(y<0)y+=MOD;x%=(MOD-1);
		ans=(ans+y*(pow_mod(2,x)-1)%MOD)%MOD;
	}
	cout<<ans<<endl;
	return 0;
}

day4-路径

题目链接

考虑在dfs整棵树的过程中顺便构造出哈密尔顿回路。

如果树是一条链,我们可以用如下方法构造(图片来自戴言老师的题解):

当树不是一条链时,我们用类似的方法构造。题解给出了这一构造方法的极具概括性的描述:

对于深度为奇数的节点,我们先输出它,再遍历它的整个子树;对于深度为偶数的节点,我们先遍历它的整个子树,再输出它。

可以验证,使用这种构造方法,输出中连续的两个节点,在树上的距离不会超过\(3\)。下图是距离等于\(3\)的一种示例,其中3号边(标为蓝色)距离为\(3\)。显然,这是能达到的最大距离了。

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

参考代码(片段):

const int MAXN=3e5;
struct EDGE{int nxt,to;}edge[MAXN*2+5];
int n,head[MAXN+5],tot,ans[MAXN+5],cnt;
inline void add_edge(int u,int v){edge[++tot].nxt=head[u];edge[tot].to=v;head[u]=tot;}
void dfs(int u,bool fir,int fa){
	if(fir)ans[++cnt]=u;
	for(int i=head[u];i;i=edge[i].nxt)if(edge[i].to!=fa)dfs(edge[i].to,fir^1,u);
	if(!fir)ans[++cnt]=u;
}
int main() {
	cin>>n;
	for(int i=1,u,v;i<n;++i)cin>>u>>v,add_edge(u,v),add_edge(v,u);
	cout<<"Yes"<<endl;
	dfs(1,1,0);for(int i=1;i<=n;++i)cout<<ans[i]<<" \n"[i==n];
	return 0;
}

day4-魔法

题目链接

对所有\(T\)串,建出AC自动机。对自动机上每个节点,记录一个值\(len[u]\),表示匹配到节点\(u\)\(T\)串中长度最小的串长度为多少。这里“匹配到节点\(u\)”,指的是该\(T\)串是根节点到\(u\)的路径所组成的串的一个后缀。特别地,如果没有串匹配到节点\(u\),则令\(len[u]=\inf\)

\(S\)求出一个数组\(lim[1\dots n+1]\)\(lim[i]\)表示\(S\)\(1\dots i-1\)位中,最后一个被删掉的位置,不能早于\(lim[i]\)。换句话说,\(lim[i]\sim i-1\)这段区间内,至少有一个位置需要被删除。特别地,如果对位置\(i\)没有限制,我们令\(lim[i]=0\),相当于默认第\(0\)个位置是已经被删除的。如何求\(lim\)数组?可以让\(S\)在AC自动机上走一遍,设\(S\)的前\(i\)位走到AC自动机上的节点\(u_i\),则:\(lim[i+1]=\max(0,i-len[u_i]+1)\)

当然,由于良心的出题人设置了\(m\leq10\)的条件,如果你不会AC自动机,求\(lim\)数组的过程也可以通过对每个\(T\)分别做KMP来实现。这里不再赘述。

求出\(lim\)数组后,我们考虑DP。设\(dp[i][j]\)表示考虑了前\(i\)位,最后一个被删除的位置为\(j\)的最小代价。转移时,分三种情况:

  • 对于\(lim[i]\leq j\leq i-1\)\(dp[i][j]=dp[i-1][j]\)
  • 对于\(j=i\)\(dp[i][j]=\min_{k=lim[i]}^{i-1}dp[i-1][k]+a[i]\)
  • 对于其他的\(j\)\(dp[i][j]=\inf\)

发现,从\(i-1\)\(i\)的转移,对DP数组第二维的修改,相当于把一段前缀(\([0,lim[i]-1]\))赋值为\(\inf\),再对位置\(i\)做一个单点修改。这可以用线段树实现。

时间复杂度\(O(\sum|T|+n\log n)\)。如果前半部分用KMP实现,则复杂度变为\(O(\sum_{i=1}^{m}(|S|+|T_i|)+n\log n)\)

参考代码(片段):

const int MAXN=2e5;
int n,m,a[MAXN+5],lim[MAXN+5];
char s[MAXN+5],t[MAXN+5];

namespace AC{
const int MAXN=2e6;
int tot,tr[MAXN+5][26],len[MAXN+5],fa[MAXN+5];

void insert_string(char* s,int n){
	int u=1;
	for(int i=1;i<=n;++i){
		if(!tr[u][s[i]-'a'])tr[u][s[i]-'a']=++tot,len[tot]=(::n)+1;
		u=tr[u][s[i]-'a'];
	}
	len[u]=min(len[u],n);
}
void build(){
	for(int i=0;i<26;++i)tr[0][i]=1;
	queue<int>q;q.push(1);
	while(!q.empty()){
		int u=q.front();q.pop();
		len[u]=min(len[u],len[fa[u]]);
		for(int i=0;i<26;++i){
			if(tr[u][i]){
				fa[tr[u][i]]=tr[fa[u]][i];
				q.push(tr[u][i]);
			}
			else tr[u][i]=tr[fa[u]][i];
		}
	}
}
void init(){
	tot=1;
	len[0]=len[1]=(::n)+1;
}
}//namespace AC

const int INF=1e9;
struct SegmentTree{
	int val[(MAXN+1)*4+5];
	bool tag[(MAXN+1)*4+5];
	void push_up(int p){
		val[p]=min(val[p<<1],val[p<<1|1]);
	}
	void push_down(int p){
		if(tag[p]){
			val[p<<1]=INF;
			tag[p<<1]=1;
			val[p<<1|1]=INF;
			tag[p<<1|1]=1;
			tag[p]=0;
		}
	}
	void build(int p,int l,int r){
		if(l==r){
			if(l==0)val[p]=0;
			else val[p]=INF;
			return;
		}
		int mid=(l+r)>>1;
		build(p<<1,l,mid);
		build(p<<1|1,mid+1,r);
		push_up(p);
	}
	void setINF(int p,int l,int r,int ql,int qr){
		if(ql<=l&&qr>=r){
			val[p]=INF;
			tag[p]=1;
			return;
		}
		push_down(p);
		int mid=(l+r)>>1;
		if(ql<=mid)setINF(p<<1,l,mid,ql,qr);
		if(qr>mid)setINF(p<<1|1,mid+1,r,ql,qr);
		push_up(p);
	}
	void point_change(int p,int l,int r,int pos,int v){
		if(l==r){
			val[p]=v;
			return;
		}
		push_down(p);
		int mid=(l+r)>>1;
		if(pos<=mid)point_change(p<<1,l,mid,pos,v);
		else point_change(p<<1|1,mid+1,r,pos,v);
		push_up(p);
	}
	SegmentTree(){}
}T;

int main() {
	cin>>n>>m;
	cin>>(s+1);
	for(int i=1;i<=n;++i)cin>>a[i];
	AC::init();
	for(int i=1;i<=m;++i){
		cin>>(t+1);
		int len=strlen(t+1);
		AC::insert_string(t,len);
	}
	AC::build();
	int u=1;
	for(int i=1;i<=n;++i){
		u=AC::tr[u][s[i]-'a'];
		lim[i+1]=max(0,i-AC::len[u]+1);
	}
	//for(int i=1;i<=n+1;++i)cout<<lim[i]<<" ";cout<<endl;
	
	/*
	static int f[5005][5005];
	memset(f,0x3f,sizeof(f));
	f[0][0]=0;
	for(int i=1;i<=n;++i){
		for(int j=lim[i];j<i;++j){
			f[i][j]=f[i-1][j];
			f[i][i]=min(f[i][i],f[i-1][j]+a[i]);
		}
	}
	int ans=INF;
	for(int i=lim[n+1];i<=n;++i)ans=min(ans,f[n][i]);
	cout<<ans<<endl;
	*/
	
	T.build(1,0,n);
	for(int i=1;i<=n+1;++i){
		if(lim[i]){
			T.setINF(1,0,n,0,lim[i]-1);
		}
		if(i==n+1)break;
		T.point_change(1,0,n,i,T.val[1]+a[i]);
	}
	cout<<T.val[1]<<endl;
	return 0;
}

相关题目推荐:

CF1327F AND Segments 记录每个位置最早能转移的点\(lim[i]\),并用线段树优化二维DP,这两个套路都和本题很像。我的题解

day4-交集

题目链接

容易发现\(u\), \(v\)是两个独立的问题,即:我们只需要在\(u\)处选\(k\)个点,在\(v\)处选\(k\)个点,然后把方案数相乘即可。具体地讲,这里在\(u\)处选\(k\)个点,指的是:令\(u\)为根,在树上任选\(k\)个点,使他们两两的LCA都为\(u\)。另外,设\(v\)\(u\)的儿子\(w\)的子树内,则整个\(w\)子树里的点都是不能选的。当然,在具体实现时,我们不可能每次询问都真的给整棵树换个根。我们在开始询问前以\(1\)为根做好预处理,然后当需要以\(u\)为根时,我们把\(u\)连向\(fa(u)\)的边也当做\(u\)的一个儿子即可。

问题转化为:如何在树上选择\(k\)个点使他们两两的LCA都为\(u\)?显然,要使两两的LCA都是\(u\),那么在\(u\)的每个儿子的子树里就至多只能选择\(1\)个点。因为儿子只有\(\leq L\)个,我们对每个\(u\)做一个背包,就能求出,在当前\(u\)中,选择\(k\)(\(k\leq L\))个点的方案数。当然,还可以选择\(u\)本身,并且可以选多次,所以我们枚举\(u\)被选了多少次,乘一个组合数即可。

但是,注意到还有一个要求:不能取(以\(u\)为根时)\(v\)所在子树内的点。前面做的背包中并没有考虑到这个要求。

我们思考这个背包的实质,它相当于是下列生成函数\(x^k\)项前的系数:

\[P_u(x)=\prod_{w\in son(u)}(1+\text{size}_w\cdot x) \]

现在要求不能取(以\(u\)为根时)\(v\)所在子树内的点,则生成函数应当变为:

\[P_{u,v}(x)=\prod_{\begin{gather*}w\in son(u)\\v\text{ not in subtree }w\end{gather*}}(1+\text{size}_w\cdot x) \]

容易发现,\(P_{u,v}(x)\)就是\(P_u(x)\)除掉了一个\((1+\text{size}_w\cdot x)\),其中\(w\)\(u\)\(v\)方向的出边。可以发现,多项式除单项式,就是背包的过程倒过来(加法变成减法)。利用预处理好的\(P_u(x)\),可以在\(O(L)\)的时间内求出\(P_{u,v}(x)\)

时间复杂度\(O((n+q)L)\)

参考代码(片段):

const int MOD=998244353,MAXN=1e5;
int fac[MAXN+5],invf[MAXN+5];
inline int mod(int x){return x<MOD?(x<0?x+MOD:x):x-MOD;}
inline int pow_mod(int x,int i){int y=1;while(i){if(i&1)y=(ll)y*x%MOD;x=(ll)x*x%MOD;i>>=1;}return y;}
inline int down_pow(int n,int k){return (ll)fac[n]*invf[n-k]%MOD;}//下降幂

struct EDGE{int nxt,to;}edge[MAXN*2+5];
int head[MAXN+5],tot;
inline void add_edge(int u,int v){edge[++tot].nxt=head[u];edge[tot].to=v;head[u]=tot;}
int n,an[MAXN+5][18],sz[MAXN+5],dep[MAXN+5];
void dfs(int u){
	dep[u]=dep[an[u][0]]+1;
	sz[u]=1;
	for(int i=1;i<18;++i)an[u][i]=an[an[u][i-1]][i-1];
	for(int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==an[u][0])continue;
		an[v][0]=u;
		dfs(v);
		sz[u]+=sz[v];
	}
}
int lca(int u,int v){
	if(dep[u]<dep[v])swap(u,v);
	for(int i=17;~i;--i)if(dep[an[u][i]]>=dep[v])u=an[u][i];
	if(u==v)return u;
	for(int i=17;~i;--i)if(an[u][i]!=an[v][i])u=an[u][i],v=an[v][i];
	return an[u][0];
}
int kth_an(int v,int k){
	for(int i=17;~i;--i)if(k&(1<<i))v=an[v][i];
	return v;
}
int q,L,deg[MAXN+5],dp[MAXN+5][505],f[505];
int solve(int u,int v,int k){
	int w=lca(u,v);
	//cout<<u<<" "<<v<<" lca:"<<w<<endl;
	int ban=(u==w?kth_an(v,dep[v]-dep[w]-1):an[u][0]);
	int x=(ban!=an[u][0]?sz[ban]:n-sz[u]);
	f[0]=1;
	int ans=1;
	for(int i=1;i<deg[u]&&i<=k;++i){
		f[i]=mod(dp[u][i]-(ll)f[i-1]*x%MOD);
		ans=mod(ans+(ll)f[i]*down_pow(k,i)%MOD);
	}
	//cout<<"-- "<<ans<<endl;
	return ans;
}
int main() {
	cin>>n>>q>>L;
	fac[0]=1;for(int i=1;i<=n;++i)fac[i]=(ll)fac[i-1]*i%MOD;
	invf[n]=pow_mod(fac[n],MOD-2);
	for(int i=n-1;~i;--i)invf[i]=(ll)invf[i+1]*(i+1)%MOD;assert(invf[0]==1);
	for(int i=1,u,v;i<n;++i)cin>>u>>v,add_edge(u,v),add_edge(v,u);
	dfs(1);
	for(int i=1;i<=n;++i){
		dp[i][0]=1;
		for(int j=head[i];j;j=edge[j].nxt){
			int x=(edge[j].to!=an[i][0]?sz[edge[j].to]:n-sz[i]);
			for(int k=deg[i];~k;--k)dp[i][k+1]=mod(dp[i][k+1]+(ll)dp[i][k]*x%MOD);
			++deg[i];
		}
	}//O(nL)
	while(q--){
		int u,v,k;cin>>u>>v>>k;
		cout<<(ll)solve(u,v,k)*solve(v,u,k)<<endl;
	}
	return 0;
}

day5-染色

题目链接

把白边染成黑色,相当于删除一条边。题目要求我们预先删掉一些边,使得剩下图上的边,能用题目要求的操作全部删完。

容易发现,一张图上,所有边能用题目要求的操作全部删完,当且仅当这张图中不存在环。

证明:

必要性:只要存在一个环,环上所有点度数至少为\(2\),这个环一定删不掉。

充分性:在没有环时,图是一个森林。对于一棵树,每次删掉所有连接叶子节点的边。必能通过有限次操作删完整棵树。

所以,只需要求出把原图变成一个森林,最少要删多少条边。

我们一边读入所有的边,一边用并查集维护所有点的连通性。如果当前边的两个端点\((u,v)\)已经联通,说明当前边需要删除。否则,当前边可以保留,我们用并查集把\((u,v)\)并起来。这和kruskal算法的原理是一样的。另外,显然的是,加边的顺序并不影响答案。

我们也可以更本质地考虑这个问题。设图中的连通块数为\(c\),则最后剩下的森林里的边数为\(n-c\)。故答案就是\(m-(n-c)\)

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

参考代码(片段):

const int MAXN=1e5;
int n,m,fa[MAXN+5],ans;
int get_fa(int x){return fa[x]==x?x:(fa[x]=get_fa(fa[x]));}
int main() {
	cin>>n>>m;
	for(int i=1;i<=n;++i)fa[i]=i;
	for(int i=1;i<=m;++i){
		int u,v;cin>>u>>v;
		int fau=get_fa(u),fav=get_fa(v);
		if(fau==fav)ans++;
		else fa[fau]=fav;
	}
	cout<<ans<<endl;
	return 0;
}

day5-乘方

题目链接

考虑二分答案,问题转化为,判断\(\bigcup_{i=1}^{k}S(n_i)\)中有多少个数\(\leq \text{mid}\)

\(\bigcup_{i=1}^{k}S(n_i)\)容斥。我们先计算\(S(n_1),S(n_2),\dots S(n_k)\)\(\leq \text{mid}\)的数的数量之和,这样会重复计算;于是我们减去既在\(S(n_1)\)中,又在\(S(n_2)\)中......(同时出现在两个集合中)的\(\leq \text{mid}\)的数的数量之和;再加上同时出现在三个集合中的\(\leq \text{mid}\)的数的数量之和......。于是,我们要求:\(\text{check}(\text{mid})=\sum_{s\in [k]}(-1)^{|s|+1}\text{cnt}\left(\bigcap_{i\in s}S(n_i)\right)\)。其中\(\text{cnt}(s)\)表示集合\(s\)\(\leq \text{mid}\)的数的数量。

容易发现,\(\bigcap_{i\in s}S(n_i)=S(\operatorname{lcm}_{i\in s}n_i)\)

暴力的做法是直接枚举子集\(s\)。问题转化为如何对一个特定的数\(x=\operatorname{lcm}_{i\in s}n_i\),求\(\text{cnt}(S(x))\)。根据定义,\(S(x)=\{1^x,2^x,3^x,\dots\}\),显然,\(t^x<(t+1)^x\) (\(x>0\)),所以我们可以出二分最大的\(t\),满足\(t^x\leq \text{mid}\)。则\(\text{cnt}(S(x))=t\)。当然,每次二分\(t\)时,还需要做快速幂。故总复杂度为:\(O(q(\log \inf\cdot2^k\log^2 m))\),其中\(\inf\)即为最大答案,\(=10^{17}\)。无法通过subtask3。

考虑优化,发现枚举子集\(s\)后,先二分\(t\)再做快速幂是十分耗时的。我们可以利用\(\texttt{C++}\)自带的\(\texttt{pow}\)函数直接对\(\text{mid}\)\(x\)次根(\(\texttt{pow(mid,1.0/x)}\)),再用快速幂微调一下误差即可。这样,总复杂度降为\(O(q(2^k\log^2\inf))\),可以通过subtask3。

想到用DP代替暴力枚举子集。发现一个子集的贡献,只与它的\(\operatorname{lcm}\),也就是上文中的\(x\)有关。故可以设\(dp[i][j]\)表示考虑了前\(i\)个数(\(n_1,n_2,\dots n_i\)),选出的\(\operatorname{lcm}=j\)时的容斥系数之和。发现,我们只需要考虑\(j\leq 60\)的情况,因为题目保证了答案不超过\(10^{17}\),而\(2^{60}>10^{17}\)。这样做的好处,相当于把\(\operatorname{lcm}\)相同的子集放到一起计算。于是,二分时,就不必枚举\(2^k\)个子集,只需要枚举\(60\)\(\operatorname{lcm}\),效率大大提高。

时间复杂度:\(O(q(k\log\inf+\log^3\inf))\)

参考代码(片段):

const ll INF=1e17;
int m,n,a[55];
ll dp[55][61];
int lcm(int x,int y){return x/__gcd(x,y)*y;}
ll pow_check(ll x,int i,ll lim=INF){
	ll y=1;
	while(i){
		if(i&1){
			if(y>lim/x)return lim+1;
			y*=x;
		}
		if(i>1&&x>lim/x)return lim+1;
		x*=x;
		i>>=1;
	}
	return y;
//	ll y=1;
//	while(i--)if((double)y*x>lim)return lim+1;else y*=x;
//	return y;
}
ll kaif(ll a,int b){
	ll res=pow(a,1.0/b);
	if(pow_check(res,b,a)<a)res++;
	if(pow_check(res,b,a)>a)res--;
	return res;
}
int main() {
	int q;cin>>q;while(q--){
		cin>>m>>n;
		memset(dp,0,sizeof(dp));
		dp[0][1]=-1;
		for(int i=1;i<=n;++i){
			cin>>a[i];
			for(int j=1;j<=60;++j){
				dp[i][j]+=dp[i-1][j];
				int t=min(lcm(j,a[i]),60);
				dp[i][t]-=dp[i-1][j];
			}
		}
		dp[n][1]++;
		//for(int i=1;i<=20;++i)cout<<dp[n][i]<<" ";cout<<endl;
		ll l=1,r=INF;
		while(l<r){
			ll mid=(l+r)>>1,sum=0;
			for(int i=1;i<=60;++i)sum+=dp[n][i]*kaif(mid,i);
			//cout<<mid<<" "<<sum<<endl;
			if(sum>=m)r=mid;
			else l=mid+1;
		}
		cout<<l<<endl;
	}
	return 0;
}

day5-位运算

题目链接

不论是\(\operatorname{AND}\), \(\operatorname{XOR}\)还是\(\operatorname{OR}\)运算,都可以直接在值域上做FWT。时间复杂度\(O(a\log a)\),其中\(a=\max_{i=1}^{n}a_i\),即值域。

但是对于\(\operatorname{AND}\)\(\operatorname{XOR}\)运算,我们有更优秀的方法。


\(\operatorname{AND}\)运算:

从高到低按位考虑。维护一个当前可选的数的集合(可重集),初始时全部\(n\)个数都在集合中。

对于当前位,我们希望答案中它为\(1\)

  • 如果当前可选数的集合里有至少两个数这一位为\(1\),说明答案的这一位确实可以为\(1\)。因此我们一定不选这一位为\(0\)的数,把这些数从集合里删掉。然后继续考虑下一位。
  • 否则,说明无论怎么选,答案的当前位都只能是\(0\)。故可选集合不变,直接考虑下一位。

当考虑完\(23\)位之后,设集合里剩余\(s\)个数。则方案数就是\(\frac{s(s-1)}{2}\)

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


\(\operatorname{XOR}\)运算:

这是经典的异或最大值问题。可以用01Trie解决。对所有\(n\)个数建01Trie。如果给定一个整数\(x\),问在\(n\)个数中,哪个数和\(x\)异或的结果最大。我们直接在01Trie上走一遍就知道了。

在01Trie上插入所有数后,枚举以每个\(a_i\)作为\(x\),分别计算一遍,即可求出异或的最大值,以及方案数。注意一种特殊的情况:当序列里所有数都相同时,最大值为\(0\),此时我们会把\(a_i\operatorname{XOR} a_i\)也算入方案数中,所以此时方案数要减\(n\)

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

参考代码(片段):

const int MAXN=1e5;
int n,q,a[MAXN+5];
namespace solver_and{
//分治
int ansv;
ll ansc;
void solve(const vector<int>& v,int dep){
	if(dep==-1){
		ansc=(ll)v.size()*(v.size()-1)/2;
		return;
	}
	vector<int>newv;
	for(int i=0;i<SZ(v);++i)if((v[i]>>dep)&1)newv.pb(v[i]);
	if(SZ(newv)>=2){
		ansv|=(1<<dep);
		solve(newv,dep-1);
	}
	else{
		solve(v,dep-1);
	}
}
int main(){
	vector<int>v;
	for(int i=1;i<=n;++i)v.pb(a[i]);
	solve(v,22);
	cout<<ansv<<" "<<ansc<<endl;
	return 0;
}
}//namespace solver_and

namespace solver_xor{
//01Trie
int ch[MAXN*25][2],tot,cnt[MAXN*25];
void ins(int x){
	int t=1;
	for(int i=22;~i;--i){
		int d=((x>>i)&1);
		if(!ch[t][d])ch[t][d]=++tot;
		t=ch[t][d];
	}
	cnt[t]++;
}
pii fd(int x){
	int t=1,ans=0;
	for(int i=22;~i;--i){
		int d=(((x>>i)&1)^1);
		if(!ch[t][d])t=ch[t][d^1];
		else t=ch[t][d],ans^=(1<<i);
	}
	return mk(ans,cnt[t]);
}
int main(){
	tot=1;for(int i=1;i<=::n;++i)ins(::a[i]);
	pii res=mk(0,0);
	for(int i=1;i<=::n;++i){
		pii cur=fd(::a[i]);
		if(cur.fi>res.fi)res=cur;
		else if(cur.fi==res.fi)res.se+=cur.se;
	}
	if(!res.fi)res.se-=n;
	cout<<res.fi<<" "<<(res.se/2)<<endl;
	return 0;
}
}//namespace solver_xor

namespace solver_or{
//FWT or
const int SIZE=1<<23;
ll f[SIZE],cnt[SIZE];
void fwt(ll *f,int n,int flag){
	for(int i=1;i<n;i<<=1){
		for(int j=0;j<n;j+=(i<<1)){
			for(int k=j;k<i+j;++k){
				f[i+k]+=f[k]*flag;
			}
		}
	}
}
int main(){
	for(int i=1;i<=::n;++i)f[::a[i]]++,cnt[::a[i]]++;
	fwt(f,SIZE,1);
	for(int i=0;i<SIZE;++i)f[i]*=f[i];
	fwt(f,SIZE,-1);
	for(int i=SIZE-1;~i;--i){
		if(f[i]-=cnt[i]){
			cout<<i<<" "<<(f[i]/2)<<endl;
			return 0;
		}
	}
	return 114514;
}
}//namespace solver_or

int main() {
	cin>>n>>q;
	for(int i=1;i<=n;++i)cin>>a[i];
	if(q==1)return solver_and::main();
	if(q==2)return solver_xor::main();
	if(q==3)return solver_or::main();
	return 0;
}

相关题目推荐:

CF1285D Dr. Evil Underscores 和本题\(\operatorname{AND}\)运算中用到的方法类似:从高到低位考虑,维护一个可选集合。

day7-字符串

题目链接

子序列匹配问题(给定一个字符串\(s\),问它是不是另一个字符串\(t\)的子序列),有一个经典的贪心方法。逐个考虑\(s\)的每一位,设当前考虑了\(s_{1\dots i}\),在\(t\)上匹配到位置\(j\)(即\(s_{1\dots i}\)\(t_{1\dots j}\)的子序列,且\(j\)是满足这样条件的最小的\(j\))。接下来直接让\(j\)跳到\(t\)\(j\)之后的、第一个等于\(s_{i+1}\)的位置即可。如果找不到这样的位置,则说明匹配失败:\(s\)不是\(t\)的子序列。

对于本题,可以把这个贪心的过程搬到DP上。

\(dp[i][j]\),表示我们构造出的串,在\(S\), \(T\)上用上述方法贪心地匹配,分别匹配到了第\(i\)、第\(j\)个位置,所需要构造的串的最小长度。预处理出\(S\), \(T\)上每个位置之后第一个\(0\) / \(1\)在哪里出现,则可以\(O(1)\)转移。

这个DP是比较容易的。然而难点在于,如何使字典序最小呢?字典序最小,肯定是要从前往后贪心。但是这个贪心的前提又是使长度最小。我们改变一下\(dp\)数组的定义,变成:此时最少还需要构造多长的串,才能使其是\(S_{i+1\dots n}\)\(T_{j+1\dots m}\)的公共非子序列。那么在转移时,如果下一位填\(0\)转移到的长度\(\leq\)\(1\)转移到的长度,我们就让下一位填\(0\),否则让下一位填\(1\)。并且用一个数组记录下转移的路径。这种从后往前的DP,可以用记忆化搜索实现,比较方便。

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

参考代码(片段):

const int MAXN=4000,INF=0x3f3f3f3f;
char s[MAXN+5],t[MAXN+5];
int n,m,dp[MAXN+5][MAXN+5],ns[MAXN+5][2],nt[MAXN+5][2],ps[2],pt[2];
pair<int,pii>nxt[MAXN+5][MAXN+5];

int dfs(int i,int j){
	assert(i==n+1||j==m+1||s[i]==t[j]);
	if(dp[i][j]!=INF)return dp[i][j];
	
	if(i==n+1&&j==m+1)return dp[i][j]=0;
	
	dp[i][j]=dfs(ns[i][0],nt[j][0])+1;
	nxt[i][j]=mk(0,mk(ns[i][0],nt[j][0]));
	if(dfs(ns[i][1],nt[j][1])+1<dp[i][j]){
		dp[i][j]=dfs(ns[i][1],nt[j][1])+1;
		nxt[i][j]=mk(1,mk(ns[i][1],nt[j][1]));
	}
	return dp[i][j];
}
void get_res(int i,int j,string& res){
	if(i==n+1&&j==m+1)return;
	res+=(char)(nxt[i][j].fi+'0');
	get_res(nxt[i][j].se.fi,nxt[i][j].se.se,res);
}
int main() {
	cin>>n>>m>>(s+1)>>(t+1);
	ps[0]=ps[1]=n+1;
	pt[0]=pt[1]=m+1;
	ns[n+1][0]=ns[n+1][1]=n+1;
	nt[m+1][0]=nt[m+1][1]=m+1;
	for(int i=n;i>=1;--i)ns[i][0]=ps[0],ns[i][1]=ps[1],ps[s[i]-'0']=i;
	for(int i=m;i>=1;--i)nt[i][0]=pt[0],nt[i][1]=pt[1],pt[t[i]-'0']=i;
	
	memset(dp,0x3f,sizeof(dp));
	int ans0=dfs(ps[0],pt[0])+1;
	//cout<<ans0<<endl;
	string res0="0";
	get_res(ps[0],pt[0],res0);
	//cout<<res0<<endl;
	
	memset(dp,0x3f,sizeof(dp));
	memset(nxt,0,sizeof(nxt));
	int ans1=dfs(ps[1],pt[1])+1;
	//cout<<ans1<<endl;
	string res1="1";
	get_res(ps[1],pt[1],res1);
	//cout<<res1<<endl;
	
	if(ans0<=ans1)cout<<res0<<endl;
	else cout<<res1<<endl;
	return 0;
}

相关题目推荐:

CF1340B Nastya and Scoreboard

day7-序列

题目链接

考虑最终每个数的出现次数,一定是\(2^x-1\)的形式(即二进制下全是\(1\))。也就是说,对于每个数值,假设当前已经选了\(2^x-1\)个,那么下一次如果要选该数值,必定一次新增\(2^x\)个。当然,我们不一定盯着一个值选。可能由于该数值已经选了过多(\(x\)太大),导致选该数值不如选一个比它更大、但出现次数更少的数划算。更直观地讲,我们可以把每次的选择,列成一张表格,行表示值,列表示该值新增的出现次数:

我们要做的,就是在该表格中,选择尽量多的格子,使其权值和\(\leq n\)

根据贪心,我们肯定先选权值小的格子。所以可以二分我们选的最大权值,记为\(\text{mx}\)。问题转化为求所有权值权值\(\text{mx}\)的格子的权值和,然后判断是否\(\leq n\)

这个表格很特殊,它行很多,高达\(O(n)\)级别,列却只有\(O(\log n)\)级别。所以我们枚举每一列,可以\(O(1)\)算出要从这一列里选多少个。知道选多少个后,求这一列的和,就相当于\(2^x\)乘以一个等差数列,也可以\(O(1)\)计算。

需要注意的是,等于\(\text{mx}\)的数,可能一部分选,一部分不选,要注意判断。

二分出最大权值后,我们再重复一遍二分的过程,就能求出选到的数量了。

单次询问时间复杂度\(O(\log^2n)\)

参考代码(片段):

ull n,sn,sum,cnt;
bool check(ull mx){
	sum=0;cnt=0;
	--mx;
	for(int i=0;i<=60;++i){
		ull lim=mx/(1ull<<i);
		if(lim>sn||(lim+1)*lim/2>n/(1ull<<i))return false;
		sum+=(lim+1)*lim/2*(1ull<<i);
		cnt+=lim;
		if(sum>n)return false;
	}
	++mx;
	ull rest=n-sum;
	for(int i=0;i<=60;++i){
		if(mx%(1ull<<i)==0){
			if(rest<mx)break;
			rest-=mx;
			cnt++;
		}
		else break;
	}
	if(rest==n-sum)return false;
	sum=n-rest;
	return true;
}
int main() {
	int T;cin>>T;while(T--){
		cin>>n;
		sn=sqrt(n);sn<<=1;
		ull l=1,r=n;
		while(l<r){
			ull mid=(l+r+1)>>1;
			if(check(mid))l=mid;
			else r=mid-1;
		}
		check(l);
		//cout<<sum<<endl;
		cout<<cnt<<endl;
	}
	return 0;
}

day7-交换

题目链接

先假设所有数字互不相同。我们从小到大考虑每个数字。那么根据题目要求,当前数字,要么放在开头,要么放在结尾。

对于一次交换操作,我们在较小的数上计算其代价。于是,把当前数挪到前面的代价,就是其前面还未考虑过的数的数量。同理,挪到后面的代价,就是其后面还未考虑过的数的数量。容易发现,当前数无论放在前面还是放在后面,都不影响它后面数的代价,因为代价只和未考虑的数有关。所以可以贪心地:哪种移动方式代价小,就移到哪里。至于求代价,用支持带点修改、区间求和的数据结构(如线段树)简单维护即可。

当有重复的数字时,最优情况下,相同数字间是不会发生交换的:即,所有交换完成后,相同数字间的相对顺序不变。因为如果发生了交换,那么不做这次交换一定能使答案更优。但是用上述的方法,可能就会计算相同数字间的交换。为了避免这种情况,我们按每个值考虑:先把当前值的所有出现位置,都设置为“已考虑”。这样就能避免交换两个相同值的问题了。

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

参考代码(片段):

const int MAXN=3e5;
struct SegmentTree{
	int sum[MAXN*4+5];
	void build(int p,int l,int r){
		if(l==r){sum[p]=1;return;}
		int mid=(l+r)>>1;
		build(p<<1,l,mid);
		build(p<<1|1,mid+1,r);
		sum[p]=sum[p<<1]+sum[p<<1|1];
	}
	void modify(int p,int l,int r,int pos,int x){
		if(l==r){sum[p]+=x;return;}
		int mid=(l+r)>>1;
		if(pos<=mid)modify(p<<1,l,mid,pos,x);
		else modify(p<<1|1,mid+1,r,pos,x);
		sum[p]=sum[p<<1]+sum[p<<1|1];
	}
	int query(int p,int l,int r,int ql,int qr){
		if(ql>qr)return 0;
		if(ql<=l && qr>=r)return sum[p];
		int mid=(l+r)>>1,res=0;
		if(ql<=mid)res+=query(p<<1,l,mid,ql,qr);
		if(qr>mid)res+=query(p<<1|1,mid+1,r,ql,qr);
		return res;
	}
	SegmentTree(){}
}T;
int n,a[MAXN+5];
pii p[MAXN+5];
int main() {
	cin>>n;
	for(int i=1;i<=n;++i)cin>>a[i],p[i]=mk(a[i],i);
	sort(p+1,p+n+1);
	T.build(1,1,n);
	ll ans=0;
	for(int i=1;i<=n;++i){
		int j=i;
		while(j+1<=n&&p[j+1].fi==p[i].fi)++j;
		for(int k=i;k<=j;++k){
			T.modify(1,1,n,p[k].se,-1);
		}
		for(int k=i;k<=j;++k){
			int vl=T.query(1,1,n,1,p[k].se-1);
			int vr=T.query(1,1,n,p[k].se+1,n);
			ans+=min(vl,vr);
		}
		i=j;
	}
	cout<<ans<<endl;
	return 0;
}

day9-排列

题目链接

先不考虑字典序,我们先求出\(\sum_{j=1}^{n}[a_i<b_{i_j}]\)的最大值。我们把找到一对\(a_i\),\(b_j\)使得\(a_i<b_j\),称为发生了一次“匹配”。容易发现,要求的就是最大的匹配数量。有一种简单的贪心方法。将\(a\), \(b\)序列分别排序。从小到大依次考虑每个\(a_i\),让它和当前未使用过的、第一个比它大的\(b_j\)匹配,然后将这个\(b_j\)标记为已使用,继续考虑下一个\(a_{i+1}\),直到找不到这样的\(j\)为止。此时求出的,就是最大匹配数。记为\(\text{num}\)

那么如何安排,使得\(\sum_{j=1}^{n}[a_i<b_{i_j}]=\text{num}\)的前提下,让\(b\)序列的字典序最大呢?

这类最优化字典序的问题,一般采用的方法是逐位确定答案。也就是说,依次枚举每一位\(i\),再从大到小枚举\(b_i\)填什么。然后判断,如果\(b_i\)填了当前值后,\(b_{i+1\dots n}\)是否至少还存在一种填法,使答案能达到\(\text{num}\)。如果判断为“是”,则\(b_i\)就填当前值了;否则,说明\(b_i\)还需要变得更小。

如果按照一开始所说的这种贪心方法来做判断,那么每次判断的时间复杂度是\(O(n)\)的。又因为还要枚举\(i\)\(b_i\)的值,所以总时间复杂度\(O(n^3)\)。无法通过本题。

发现,当\(b_i\)从大到小变化时,每次重新判断,似乎有点太暴力了。既然枚举\(i\)\(b_i\)的值不好避免,那么就尝试优化这个判断的过程。

考虑一开始的贪心。我们从小到大,依次让每个\(a_i\)匹配第一个能匹配的\(b_j\)。还有一种和它等价的贪心:从大到小,依次让每个\(b_j\),去匹配最后一个(也就是最大的、最靠右的)能匹配的\(a_k\)。称这两个贪心分别为“第一种贪心”、“第二种贪心”。

我们枚举的\(b_i\)的值是从大到小变化的。最开始,也就是\(b_i\)最大时,我们先对\(b_{i+1\dots n}\)用第一种贪心,求一遍答案。然后随着\(b_i\)逐渐变小,我们用比\(b_i\)大的这段\(b\),去做第二种贪心。假设,比\(b_i\)大的所有\(b\),它们做第二种贪心,匹配到的最后一个位置为\(a_k\)。那么,\(a_{1\dots k-1}\)仍然是用第一种贪心,这个贪心的数量,对于每个前缀\(k\),我们可以预处理出来。于是,从大到小枚举\(b_i\)取值的过程,就是整个序列,在从第一种贪心,逐步转为第二种贪心的过程。当前,这两个贪心拼合在一起,也就是任意一个中间状态,显然还是最优的,也就是和单独做某一种贪心是等价的。于是,我们就一边枚举\(b_i\)的值,一边顺便维护出了后面的最大答案,也就是完成了原本单独做一次需要\(O(n)\)的这个“判断”。

现在,总时间复杂度\(O(n^2)\)

参考代码(片段):

const int MAXN=5000;
int n,b[MAXN+5],res[MAXN+5];
pii a[MAXN+5];
bool used_b[MAXN+5];

int main() {
	cin>>n;
	for(int i=1;i<=n;++i)cin>>a[i].fi,a[i].se=i;
	for(int i=1;i<=n;++i)cin>>b[i];
	sort(a+1,a+n+1);sort(b+1,b+n+1);
	int max_num=0;
	for(int i=1,j=0;i<=n;++i){
		while(j+1<=n && a[j+1].fi<b[i])++j;
		if(j>max_num)++max_num;
	}
	//cout<<"maxnum "<<max_num<<endl;
	int cur_num=0;
	for(int i=1;i<=n;++i){
		//逐位确定b序列
		static int aa[MAXN+5];
		static pii bb[MAXN+5],pre[MAXN+5];
		int cnt_a=0,ai=0,cnt_b=0;
		for(int j=1;j<=n;++j){
			if(a[j].se>i)aa[++cnt_a]=a[j].fi;
			if(a[j].se==i)ai=a[j].fi;
			if(!used_b[j])bb[++cnt_b]=mk(b[j],j);
		}
		int tmp_num=0;
		for(int j=1,k=0;j<cnt_b;++j){
			while(k+1<=cnt_a && aa[k+1]<bb[j].fi)++k;
			if(k>tmp_num){
				++tmp_num;
				pre[tmp_num]=mk(tmp_num,j);
			}
		}
		for(int j=tmp_num+1;j<=cnt_a;++j)
			pre[j]=pre[j-1];
		if(cur_num+tmp_num+(ai<bb[cnt_b].fi)==max_num){
			res[i]=bb[cnt_b].fi;
			used_b[bb[cnt_b].se]=1;
			cur_num+=(ai<bb[cnt_b].fi);
			continue;
		}
		tmp_num=0;
		for(int j=cnt_b-1,k=cnt_a,l=cnt_a;j>=1;--j){
			while(k>=1 && aa[k]>=bb[j+1].fi)--k;
			if(k>=1)--k,++tmp_num;
			l=min(l,k);
			while(l>=1 && pre[l].se>=j)--l;
			if(cur_num+pre[l].fi+tmp_num+(ai<bb[j].fi)==max_num){
				res[i]=bb[j].fi;
				used_b[bb[j].se]=1;
				cur_num+=(ai<bb[j].fi);
				break;
			}
		}
		assert(res[i]!=0);
	}
	for(int i=1;i<=n;++i)
		cout<<res[i]<<" \n"[i==n];
	return 0;
}

day9-分组

题目链接

先把所有学生,按\(s_i\)从小到大排序(下文所说的\(s\),都是排好序后的序列)。那么,每一组的极差,就相当于是本组最后一个学生的\(s\)值减去本组第一个学生的\(s\)值。我们把每组的第一个和最后一个学生称为本组的“开头”和“结尾”。依次考虑每个学生,则当前的小组,可以分为“已经结尾的”和“还未结尾的”。也就是说,对于“已经结尾的”小组,它的“结尾”,肯定是已经考虑过的某个学生;而另一种小组的“结尾”,则是之后的某个学生。

于是可以做一个DP。设\(dp[i][j][S]\),表示考虑了前\(i\)个学生,当前还未结尾的小组有\(j\)个,此时的极差之和为\(S\)的方案数。这里的“极差之和”\(S\),其实具体来讲,应该说是前\(i\)个学生,对极差的贡献之和。每个学生对极差的贡献,前面已经提到:如果他是一组的开头,则贡献为\(-s_i\);如果是一组的结尾,则贡献为\(s_i\);否则贡献为\(0\)。据此可以写出三种转移:

  • 如果当前学生是某一组的开头。那么我们让\(j\)增大\(1\)\(S\)减小\(s_i\)
  • 如果当前学生是某一组的结尾。那么我们让\(j\)减小\(1\)\(S\)增大\(s_i\)。转移时,要乘以系数,也就是在前面任选一组加入的方案数,为\(j\)
  • 如果当前学生,既不是开头,也不是结尾(或者他所在的学习小组里只有他一个人),则\(j\)\(S\)都不变。转移时要乘以系数\(j+1\),因为既可能新加入前面\(j\)组中的一组,也可能自己作为一个“单人组”。

最终答案就是:\(dp[n][0][0\dots k]\)之和。

直接DP的时间复杂度为\(O(n^2\sum s)\)。无法通过本题。

考虑优化。可以发现因为最终我们只需要询问\(S=0\dots k\)的位置,那么一个自然的想法是如果\(S\)超出\(k\)我们直接把这个状态丢弃掉。但是因为我们在转移的过程中,\(S\)同样可能会变小,所以直接做并不可行。

我们要想个办法,让\(S\)在转移时只有加、没有减。也就是说,每个位置对极差的“贡献”都要是一个非负数。考虑做差分!设\(d_i=s_i-s_{i-1}\) (\(i>1\))。那么对于一个学习小组,设开头为\(i\),结尾为\(j\),它的极差就是:\(\sum_{t=i+1}^{j}d_t\)。也就是说,我们把贡献摊到了每个\(d\),并且\(d\)数组显然是非负的,于是就可以舍弃掉第三维大于\(k\)的状态了!状态数精简为\(O(n^2k)\)

转移时,新的第三维\(S'\)就等于\(S+d_i\cdot j\)

时间复杂度\(O(n^2k)\)

参考代码(片段):

const int MOD=1e9+7;
inline int mod1(int x){return x<MOD?x:x-MOD;}
inline int mod2(int x){return x<0?x+MOD:x;}
inline void add(int& x,int y){x=mod1(x+y);}
inline void sub(int& x,int y){x=mod2(x-y);}
inline int pow_mod(int x,int i){int y=1;while(i){if(i&1)y=(ll)y*x%MOD;x=(ll)x*x%MOD;i>>=1;}return y;}

const int MAXN=500,MAXK=1000;
int n,k,a[MAXN+5],dp[2][MAXN+5][MAXK+5];


int main() {
	cin>>n>>k;
	for(int i=1;i<=n;++i)cin>>a[i];
	sort(a+1,a+n+1);
	dp[0][0][0]=1;
	for(int i=1,cur=1;i<=n;++i,cur^=1){
		int pre=cur^1;
		memset(dp[cur],0,sizeof(dp[cur]));
		for(int j=0;j<i;++j){
			for(int S=0;S<=k;++S)if(dp[pre][j][S]){
				int newS=S+j*(a[i]-a[i-1]);
				if(newS>k)break;
				//开头
				add(dp[cur][j+1][newS],dp[pre][j][S]);
				//结尾
				if(j)add(dp[cur][j-1][newS],(ll)dp[pre][j][S]*j%MOD);
				//既非开头 也非结尾
				add(dp[cur][j][newS],(ll)dp[pre][j][S]*(j+1)%MOD);
			}
		}
	}
	int cur=n&1,ans=0;
	for(int S=0;S<=k;++S)add(ans,dp[cur][0][S]);
	cout<<ans<<endl;
	return 0;
}

day9-异或

题目链接

引理:

我们定义一个点的“点权”为它所有出边(含它与父亲之间的边)的边权异或和。那么,“所有边权都为\(0\)”,就等价于“所有点权都为\(0\)”,它们互为充分必要条件。

证明:“所有点权为\(0\)”是“所有边权为\(0\)”的充分必要条件。

【必要性】比较显然。根据点权的定义,边权均为\(0\)时,所有点点权一定为\(0\)

【充分性】对\(n\)归纳。\(n=1\)时显然成立。\(n>1\)时,若对\(n-1\)成立,我们考虑添加一个叶子。因为点权为\(0\),且叶子只有一条出边,所以叶子的边权也为\(0\)。所以整棵树边权都为\(0\)

考虑一次操作对点权的影响,发现相当于选择两个点\(u\), \(v\),然后让\(u\), \(v\)的点权同时异或上一个数\(x\)

于是问题转化为,有\(n\)个数\(a_1,a_2,\dots a_n\),每次可以选择两个数\(a_u\), \(a_v\),把它们异或上同一个值。目标是让所有数字均为\(0\),求最小操作次数。

先考虑怎么让它们全变成\(0\)。暴力的做法是从左到右,依次把每个数消成\(0\)。那最后一个数怎么办呢?其实,最后一个数不用做任何操作,轮到它时,它一定是\(0\)。这是因为,树上每条边在恰好两个点的点权里出现,所以\(a_1\dots a_n\)的异或和一定为\(0\)

所以,最多只需要做\(n-1\)次操作,就能把所有数都变为\(0\)。但这样不一定是最优的。经过上面的分析,可以发现,对于任意\(k\)个异或和为\(0\)的数,只需要\(k-1\)次操作,就能将它们全部变为\(0\)。因此,我们要把\(a\)序列分为尽可能多的、异或和为\(0\)的组,因为每分出一组,就能使答案减少\(1\)

由于边权的范围是\([0,15]\),所以点权一定也在这个范围内。首先,等于\(0\)的数不用管,因为它既不需要我们操作,也不会对其他数的操作产生贡献。其他数字,如果出现次数大于等于\(2\)次,那么一定每两个放到一组(反证法,如果两个相同的数,被分到两个不同的组,则可以把这两个相同的数放到一起,原来它们各自所在的组放到一起)。

所以,每种值,最后只会剩\(1\)个或\(0\)个(取决于他们原本的数量是奇数还是偶数)。我们只需要对这不超过\(15\)个数分组。可以做状压DP。设\(dp[s]\)表示已经考虑了\(s\)里的数,最多能分出多少组。预处理出每个集合是否异或和为\(0\)。转移时,枚举\(s\)的一个异或和为\(0\)的子集\(t\),用\(dp[s\setminus t]\)更新\(dp[s]\)

时间复杂度\(O(n+3^w)\),其中\(w\)为权值,\(w\leq 15\)

参考代码(片段):

const int MAXN=1e5;
int n,m,a[MAXN+5],c[16],v[17],s[1<<16],dp[1<<16],ans;
int main() {
	cin>>n;
	for(int i=1,u,v,w;i<n;++i){
		cin>>u>>v>>w;
		a[u]^=w,a[v]^=w;
	}
	for(int i=1;i<=n;++i)c[a[i]]++;
	for(int i=1;i<16;++i){
		ans+=(c[i]>>1);
		if(c[i]&1)v[++m]=i;
	}
	for(int i=0;i<(1<<m);++i){
		for(int j=1;j<=m;++j){
			if(i&(1<<(j-1)))s[i]^=v[j];
		}
	}
	memset(dp,0x3f,sizeof(dp));
	dp[0]=0;
	for(int i=1;i<(1<<m);++i){
		if(s[i]==0)dp[i]=__builtin_popcount(i)-1;
		for(int j=i;j;j=i&(j-1)){
			assert((i&j)==j && (i|j)==i);//j是i的一个子集
			if(s[j]==0)dp[i]=min(dp[i],dp[i^j]+__builtin_popcount(j)-1);
		}
	}
	cout<<ans+dp[(1<<m)-1]<<endl;
	return 0;
}

day10-旅行

题目链接

考虑二分答案\(\text{mid}\)。那么,此时距离\(\geq 2\cdot \text{mid}\)的点之间就可以通过,否则就不能通过。我们反过来考虑:在所有距离\(<2\cdot \text{mid}\)的点之间连边,把上、下边界也看做两个“点”,那么,如果上、下边界之间连通,就说明有一段路被堵死了,当前\(\text{mid}\)无解。否则\(\text{mid}\)就是可以的。

但是二分答案复杂度太高。我们考虑这个过程等价于什么。

对于这张\(k+2\)个点的图,我们定义两点间的边权是它们欧几里得距离的一半。那么二分\(\text{mid}\)后,相当于只考虑所有边权\(<2\cdot \text{mid}\)的边,问有没有从上边界点\(k+1\))到下边界点\(k+2\))的路径。

在原图上,我们定义一条路径的长度,是路径上所有边边权的最大值。那么,\(\text{mid}\)无解,当且仅当存在一条从\(k+1\)\(k+2\)的路径长度\(\leq \text{mid}\)(这样就被堵死了)。所以我们不用二分\(\text{mid}\),而是直接求从\(k+1\)\(k+2\)的最短路长度即可!

因为这个图非常特殊,点数很少而边数很多,所以可以直接用不加堆优化的dijkstra算法,时间复杂度\(O(k^2)\)

参考代码(片段):

const int MAXK=7000;
int n,m,K;
double dis[MAXK+5];
bool vis[MAXK+5];
struct Point_t{
	int x,y;
}p[MAXK+5];
double get_dist(double x1,double y1,double x2,double y2){
	return sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
}
int main() {
	cin>>n>>m>>K;
	for(int i=1;i<=K;++i){
		cin>>p[i].x>>p[i].y;
		dis[i]=m-p[i].y;
	}
	dis[0]=m;
	while(true){
		int u=0;
		for(int i=1;i<=K;++i)if(!vis[i] && dis[i]<dis[u])u=i;
		if(!u){
			cout<<setiosflags(ios::fixed)<<setprecision(10)<<dis[u]/2<<endl;
			return 0;
		}
		vis[u]=1;
		dis[0]=min(dis[0],max(dis[u],(double)p[u].y));
		for(int i=1;i<=K;++i)if(!vis[i]){
			dis[i]=min(dis[i],max(dis[u],get_dist(p[i].x,p[i].y,p[u].x,p[u].y)));
		}
	}
	return 114514;
}

day10-寻宝

题目链接

乱搞做法:

二分答案。

check时,每一轮,先把所有点把所有点random_shuffle一下。然后依次考虑每个点,如果从当前边界能走到该点,就直接走过去,然后立即更新边界,继续考虑后面的点。如果所有点都访问过,直接反回\(\texttt{true}\)。如果这一轮没走到任何点(边界没有被更新过),反回\(\texttt{false}\)。否则进行下一轮。

最坏时间复杂度是\(O(n^2\log x)\)。但可以AC。

参考代码(片段):

bool check(long long mid){
	memset(vis,0,sizeof(vis));
	for(int i=1;i<=n;++i){
		if(p[i].x<=1 && p[i].y<=1){
			vis[p[i].id]=1;
		}
	}
	int curx=1,cury=1;
	while(true){
		random_shuffle(p+1,p+n+1);
		bool allvis=true;
		bool newpoint=false;
		for(int i=1;i<=n;++i){
			if(vis[p[i].id])continue;
			allvis=false;
			if(2LL*(max(0,p[i].x-curx)+max(0,p[i].y-cury))<=mid){
				vis[p[i].id]=1;
				newpoint=true;
				curx=max(curx,p[i].x);
				cury=max(cury,p[i].y);
			}
		}
		if(allvis)return true;
		if(!newpoint)return false;
	}
}

正解:

在任意的时刻,我们可以把“宝藏”分为四类:(1) 位于已经探索过的区域里的宝藏,(2) 位于区域右上角,(3) 位于区域正上方,(4) 位于区域正右方。

第(1)种可以直接加入,不需要新花费木棒。对于后面三种,我们想到一个贪心策略:算出加入每个点,需要新花费的木棒数,然后选花费最小的加入,并更新区域边界。考虑为什么这样贪心是对的。因为随着加点过程的进行,边界只会扩大不会缩小,所以加入每个点的代价只会不断减小。我们要最小化“新增数量的最大值”,所以显然每次选最小的加入是最优的。

那么问题转化为,如何快速选出花费最小的点。

考虑(2),(3),(4)三类点的花费分别是什么。假设当前已探索区域右上角为\((x_0,y_0)\),新加入的点坐标为\((x_1,y_1)\)。那么,新加入第(2)类点的花费为\(2(x_1-x_0+y_1-y_0)\)。新加入第(3)类点的花费为\(2(y_1-y_0)\)。新加入第(4)类点的花费为\(2(x_1-x_0)\)

对于第(3)类点,我们相当于对于一个横坐标的前缀,求里面\(y\)坐标最小的点。同理,对于第(4)类点,我们相当于对一个纵坐标的前缀,求里面\(x\)坐标最小的点。当然,这里面可能混杂着一些第(1)类的点,我们要支持把它们“删除”,下面会讲具体怎么做。

可以用两个小根堆维护。以第(3)类点的堆为例,这个堆里点按\(y\)坐标为关键字,每次弹出\(y\)坐标最小的。当已探索区域的\(x\)坐标扩大时,就把新加入的这些\(x\)坐标上的点放入这个堆里。当需要弹出堆顶元素时,用一个while循环,直到弹出的不是第(1)类为止(相当于用这种方法实现“把第(1)类点删除”的效果)。

对于第(2)类,可以不需要堆。一开始直接把所有点按\(x+y\)排序。用一个指针,初始时指向\(1\)。如果当前元素是第(1),(3),(4)类,就把指针\(\texttt{++}\)。直到找到第(2)类元素为止。仔细想想,第(2)类点不需要堆,是因为它们没有“横/纵坐标上一个前缀”这个限制,所以没有“加入”操作。直接用这个预先排好序的数组,指针指向的点,就相当于“堆顶”了。

每次,取(2),(3),(4)类的堆顶,比一比谁花费最小,就把谁加入。

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

参考代码(片段):

const int MAXN=3e5;
const ll INF=4e9;
int n,x[MAXN+5],y[MAXN+5];
int sorted_x[MAXN+5],sorted_y[MAXN+5],sorted_xy[MAXN+5];
bool vis[MAXN+5];
bool cmp_x(int i,int j){return x[i]<x[j];}
bool cmp_y(int i,int j){return y[i]<y[j];}
bool cmp_xy(int i,int j){return x[i]+y[i]<x[j]+y[j];}

int main() {
	cin >> n;
	for ( int i = 1 ; i <= n ; ++ i) {
		cin >> x[i] >> y[i] ;
		sorted_x[i] = sorted_y[i] = sorted_xy[i] = i ;
	}
	sort ( sorted_x + 1 , sorted_x + n + 1 , cmp_x ) ;
	sort ( sorted_y + 1 , sorted_y + n + 1 , cmp_y ) ;
	sort ( sorted_xy + 1 , sorted_xy + n + 1 , cmp_xy ) ;
	priority_queue<pii>q_x,q_y;
	int cur_x=1,cur_y=1;
	int idx_x=0,idx_y=0,idx_xy=0;
	for(int t=1;t<=n;++t){
		while(idx_x+1<=n && x[sorted_x[idx_x+1]]<=cur_x){
			++idx_x;
			q_y.push(mk(-y[sorted_x[idx_x]],sorted_x[idx_x]));
		}
		while(idx_y+1<=n && y[sorted_y[idx_y+1]]<=cur_y){
			++idx_y;
			q_x.push(mk(-x[sorted_y[idx_y]],sorted_y[idx_y]));
		}
		while(idx_xy+1<=n && (vis[sorted_xy[idx_xy+1]] || x[sorted_xy[idx_xy+1]]<=cur_x || y[sorted_xy[idx_xy+1]]<=cur_y))
			++idx_xy;
		while(!q_x.empty() && vis[q_x.top().se])q_x.pop();
		while(!q_y.empty() && vis[q_y.top().se])q_y.pop();
		
		if(!q_x.empty() && -q_x.top().fi<=cur_x){
			int res=q_x.top().se;
			cout<<res<<" ";
			vis[res]=1;
			cur_x=max(cur_x,x[res]);
			cur_y=max(cur_y,y[res]);
			continue;
		}
		if(!q_y.empty() && -q_y.top().fi<=cur_y){
			int res=q_y.top().se;
			cout<<res<<" ";
			vis[res]=1;
			cur_x=max(cur_x,x[res]);
			cur_y=max(cur_y,y[res]);
			continue;
		}
		
		pair<ll,int>res=mk(INF,0);
		if(!q_x.empty())
			res=min(res,mk(2LL*(-q_x.top().fi-cur_x),q_x.top().se));
		if(!q_y.empty())
			res=min(res,mk(2LL*(-q_y.top().fi-cur_y),q_y.top().se));
		if(idx_xy+1<=n)
			res=min(res,mk(2LL*(x[sorted_xy[idx_xy+1]]-cur_x+y[sorted_xy[idx_xy+1]]-cur_y),sorted_xy[idx_xy+1]));
		assert(res!=mk(INF,0));
		cout<<res.se<<" ";
		vis[res.se]=1;
		cur_x=max(cur_x,x[res.se]);
		cur_y=max(cur_y,y[res.se]);
	}
	return 0;
}

day10-鞋子

题目链接

我们先不考虑方向的问题,假设只要是相邻的左右脚就能匹配。那么,问题相当于求二分图最大匹配:左、右脚的格子分别是二分图的两边,两个格子相邻就连边。

然后考虑方向。如果有至少一只鞋子没有匹配,则可以通过它调整它周围所有鞋的方向。同理,也可以先通过它周围的鞋去调整更外面的鞋。所以最后一定能把每个格子调成任何我们想要的方向。

剩下的就是所有格子都匹配的情况。也就是\(nm\)为偶数,且最大匹配数为\(\frac{nm}{2}\)。根据上面的讨论,此时答案要么是\(\frac{nm}{2}\),要么是\(\frac{nm}{2}-1\)(也就是空出一双鞋子用来调整别的鞋的方向)。发现,如果我们把四个方向编号为\(0,1,2,3\),那么一次操作后,两个格子一个\(+1\),一个\(-1\)\(\bmod4\)意义下),总和\(\bmod4\)不变!于是我们猜想,所有格子如果能自己调整过来(答案等于\(\frac{nm}{2}\)),当且仅当它们总和\(\bmod 4\)与目标状态相等。而知道匹配关系以后,目标状态是很好计算的。

可以证明,这个结论是对的。具体来说,就是证明【所有完美匹配的权值和对\(4\)取模的结果相同】,以及【任意权值和对\(4\)取模后相同的状态是相互可达的】这两个结论。详见官方题解。

时间复杂度\(O(nm\sqrt{nm})\)

参考代码(片段):

const int MAXN=105,INF=1e9;
const int dx[4]={0,1,0,-1},dy[4]={1,0,-1,0};
string s1[MAXN],s2[MAXN];
int val[233];
int n,m,X1[MAXN],Y1[MAXN],X2[MAXN],Y2[MAXN];
bool inmap(int i,int j){return i>=1&&i<=n&&j>=1&&j<=m;}
int id(int i,int j){return (i-1)*m+j;}
struct EDGE{int nxt,to,w;}edge[2000005];
int head[10010],tot;
inline void add_edge(int u,int v,int w){
	edge[++tot].nxt=head[u];edge[tot].to=v;edge[tot].w=w;head[u]=tot;
	edge[++tot].nxt=head[v];edge[tot].to=u;edge[tot].w=0;head[v]=tot;
}
int dep[10010],cur[10010];
bool bfs(int s,int t){
	queue<int>q;
	q.push(s);
	for(int i=1;i<=t;++i)dep[i]=0;
	dep[s]=1;
	while(!q.empty()){
		int u=q.front();q.pop();
		for(int i=head[u];i;i=edge[i].nxt){
			int v=edge[i].to;
			if(edge[i].w&&!dep[v]){
				dep[v]=dep[u]+1;
				if(v==t)return 1;
				q.push(v);
			}
		}
	}
	return 0;
}
int dfs(int u,int flow,int t){
	if(u==t)return flow;
	int rest=flow;
	for(int &i=cur[u];i&&rest;i=edge[i].nxt){
		int v=edge[i].to;
		if(dep[v]==dep[u]+1&&edge[i].w){
			int k=dfs(v,min(rest,edge[i].w),t);
			if(!k){dep[v]=0;continue;}
			edge[i].w-=k;
			edge[i^1].w+=k;
			rest-=k;
		}
	}
	return flow-rest;
}
int eid[MAXN][MAXN][4];
int solve(){
	tot=1;int s=n*m+1,t=n*m+2;
	for(int i=1;i<=n;++i){
		for(int j=1;j<=m;++j){
			if(s1[i][j]=='L'){
				for(int k=0;k<4;++k){
					int ii=i+dx[k],jj=j+dy[k];
					if(inmap(ii,jj) && s1[ii][jj]=='R'){
						add_edge(id(i,j),id(ii,jj),1);
						eid[i][j][k]=tot;
					}
				}
			}
		}
	}
	for(int i=1;i<=n;++i){
		for(int j=1;j<=m;++j){
			if(s1[i][j]=='L')add_edge(s,id(i,j),1);
			else add_edge(id(i,j),t,1);
		}
	}
	int maxflow=0,tmp;
	while(bfs(s,t)){
		for(int i=1;i<=t;++i)cur[i]=head[i];
		while(tmp=dfs(s,INF,t))maxflow+=tmp;
	}
	return maxflow;
}
int main() {
	ios::sync_with_stdio(0);//syn!!!
	cin>>n>>m;
	for(int i=1;i<=n;++i){cin>>s1[i];s1[i]="@"+s1[i];}
	for(int i=1;i<=n;++i){cin>>s2[i];s2[i]="@"+s2[i];}
	val['U']=0;val['R']=1;val['D']=2;val['L']=3;
	int ans=solve();
	if((n*m)%2==0&&ans==n*m/2){
		int v1=0,v2=0;
		for(int i=1;i<=n;++i){
			for(int j=1;j<=m;++j){
				if(s1[i][j]=='L'){
					for(int k=0;k<4;++k){
						if(eid[i][j][k] && edge[eid[i][j][k]].w){
							//cout<<i<<" "<<j<<" "<<k<<endl;
							v1+=val[(int)s2[i][j]]+val[(int)s2[i+dx[k]][j+dy[k]]];
							v2+=k+k;
							break;
						}
					}
				}
			}
		}
		if(v1%4!=v2%4)ans--;
	}
	cout<<ans<<endl;
	return 0;
}
posted @ 2020-08-26 14:36  duyiblue  阅读(461)  评论(1编辑  收藏  举报