1. 程式人生 > 其它 >CSP2021提高組複賽解析

CSP2021提高組複賽解析

前言

終於出成績了我可以寫部落格辣,官方資料還沒出就先放洛谷的題目連結了。


正題


T1-廊橋分配

https://www.luogu.com.cn/problem/P7913

題目大意

\(m_1\)種一類飛機,\(m_2\)種二類飛機,每個飛機有一個佔用時間的區間。要給兩類飛機分配恰好\(n\)個廊橋。
如果對於一類飛機當它來到時如果有空的它這一類的廊橋就會分配給他。
求最多能容納多少飛機。

\(1\leq n\leq 10^5,1\leq m_1+m_2\leq 10^5\)

解題思路

因為飛機的策略就是能停就停,我們可以考慮貪心策略。
先考慮單類的飛機,假設分配的廊橋為\(k\),當一輛飛機不能進入當且僅當現在\(k\)

個廊橋已經被霸佔了,此時如果需要停靠這倆飛機就需要新開一個廊橋。
我們可以設有無數個廊橋,然後我們優先分配編號小的廊橋,然後最後如果有\(k\)個廊橋時答案就是在\(1\sim k\)的廊橋排列的飛機數。
具體的做法對於兩類各做一次,用一個優先佇列維護現在所有被霸佔的廊橋的恢復時間,然後用一個set維護現在空餘的廊橋編號就可以了。

時間複雜度:\(O(n\log n)\)

code

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<set>
#include<queue>
#define mp(x,y) make_pair(x,y)
using namespace std;
const int N=1e5+10;
struct node{
	int l,r;
}a[N];
int n,m,f[N],g[N];set<int> s;
priority_queue<pair<int,int> > q;
bool cmp(node x,node y)
{return x.l<y.l;}
int main()
{
	int pm;
	scanf("%d%d%d",&n,&m,&pm);
	for(int i=1;i<=max(n,max(m,pm));i++)s.insert(i);
	for(int i=1;i<=m;i++)scanf("%d%d",&a[i].l,&a[i].r);
	sort(a+1,a+1+m,cmp);
	for(int i=1;i<=m;i++){
		while(!q.empty()&&-q.top().first<=a[i].l)
			s.insert(q.top().second),q.pop();
		int x=*s.begin();f[x]++;s.erase(x);
		q.push(mp(-a[i].r,x));
	}
	while(!q.empty())s.insert(q.top().second),q.pop();
	for(int i=1;i<=n;i++)f[i]+=f[i-1];
	m=pm;
	for(int i=1;i<=m;i++)scanf("%d%d",&a[i].l,&a[i].r);
	sort(a+1,a+1+m,cmp);
	for(int i=1;i<=m;i++){
		while(!q.empty()&&-q.top().first<=a[i].l)
			s.insert(q.top().second),q.pop();
		int x=*s.begin();g[x]++;s.erase(x);
		q.push(mp(-a[i].r,x));
	}
	while(!q.empty())s.insert(q.top().second),q.pop();
	for(int i=1;i<=n;i++)g[i]+=g[i-1];
	int ans=0;
	for(int i=0;i<=n;i++)
		ans=max(ans,f[i]+g[n-i]);
	printf("%d\n",ans);
	return 0;
}

T2-括號序列

https://www.luogu.com.cn/problem/P7914

題目大意

一個合格的括號序被定義為

然後給出帶\(*,(,),?\)的字串,然後求有多少種把\(?\)切換成\((,),*\)的方案使得是一個合法的括號序。
\(1\leq k\leq n\leq 500\)

解題思路

開始考慮一個一個填發現不行。
然後這個複雜度考慮區間\(dp\),設\(f_{l,r}\)表示區間\(l\sim r\)合法的方案。
然後考慮怎麼轉移,先維護一個\(s_{l,r}\)表示\(l\sim r\)是否能夠湊成一個長度不超過\(k\)的全\(*\)序列。
對於第\(1\)種和第\(3\)

種情況,先看下\(l,r\)是否能是\('('\)\(')'\)的形式,然後第一種情況就直接加\(s_{l+1,r-1}+f_{l+1,r-1}\)(對應\(S/A\)),第三種我們可以列舉\(k\in[l+1,r-1)\),然後轉移\(f_{l+1,k}\times s_{k+1,r}+s_{l+1,k}\times f_{k+1,r}\)(對應了\(AS/SA\))就好了。
\(2\)種情況比較麻煩,我們需要列舉一個\(l\leq L<R\leq r\)然後中間填\(S\),就是\(f_{l,L}\times f_{R,r}\times s_{L+1,R-1}\)。但是這個列舉比較慢,因為對於一個\(r\),滿足\(s_{l,r}=1\)\(l\)肯定是一個到\(r\)的區間,並且\(r\)向右移動時這個區間也向右移動,所以可以使用一個字首和優化。

發現這樣還是過不了樣例,問題出在如果存在\(ASASA\)的情況,此時會被統計兩次(\(|ASA|SA\)\(AS|ASA|\)各一次),更多的同理,所以我們可以設\(g_{l,r}\)表示不帶情況二時的合法方案,然後在後面轉移就好了。

code

#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
const ll N=510,P=1e9+7;
ll n,k,f[N][N],g[N][N],S[N][N];char s[N];
signed main()
{
	scanf("%lld%lld",&n,&k);
	scanf("%s",s+1);
	for(ll i=1;i<=n;i++){
		S[i][i-1]=1;
		for(ll j=i;j<=min(n,i+k-1);j++){
			S[i][j]=S[i][j-1]&(s[j]=='?'||s[j]=='*');
			if(!S[i][j])break;
		}
	}
	for(ll len=2;len<=n;len++)
		for(ll l=1;l<=n-len+1;l++){
			ll r=l+len-1;
			if((s[l]=='?'||s[l]=='(')&&(s[r]=='?'||s[r]==')')){
				(f[l][r]+=S[l+1][r-1]+f[l+1][r-1])%=P;
				for(ll k=l+1;k<r-1;k++)
					(f[l][r]+=f[l+1][k]*S[k+1][r-1]+f[k+1][r-1]*S[l+1][k])%=P;
			}
			ll sum=0,z=l;g[l][r]=f[l][r];
			for(ll k=l;k<r;k++){
				(sum+=f[l][k])%=P;
				while(!S[z+1][k])(sum-=f[l][z])%=P,z++;
				(f[l][r]+=g[k+1][r]*sum%P)%=P;
			}
		}
	printf("%lld\n",(f[1][n]+P)%P);
	return 0;
}

T3-迴文

https://www.luogu.com.cn/problem/P7915

題目大意

有一個長度為\(2n\)的序列\(a\),保證\(1\sim n\)都各出現了兩次,你有兩種操作

  • \(a\)的開頭新增到序列\(b\)的末尾並在\(a\)移除。
  • \(a\)的末尾新增到序列\(b\)的末尾並在\(a\)移除。

一操作為\(L\),二操作為\(R\),要求使得最終\(b\)迴文的情況下操作序列的字典序最小。

\(1\leq T\leq 100,\sum n\leq 5\times 10^5\)

解題思路

顯然第一個丟進\(b\)的肯定是第一個或者最後一個,我們先假設是第一個且數字為\(x\),那麼最後被丟進去的肯定是另一個\(x\)

然後我們可以從這個\(x\)的位置開始作為一個區間,然後開始每次你丟進去的下一個數都必須在這個區間的左右,然後再用這個區間再擴充套件丟進去的數的另一個的對應位置。

但是這樣暴力搜丟左邊還是右邊是\(2^n\)的顯然不行,但是我們發現假設如果一個情況左右都能丟,在字典序最小的情況下我們肯定是先丟左邊的,而此時丟了之後不會導致右邊不能丟了,所以此時丟左邊肯定是最優的。

所以其實這樣搜是\(O(n)\)的,時間複雜度\(O(\sum n)\)


T4-交通規劃

https://www.luogu.com.cn/problem/P7916

題目大意

有一個\(n\)條水平線和\(m\)條垂直線交叉形成\(n\times m\)個格點的圖,把所有的邊按照順時針排序如圖。
每個格點之間有邊權。
\(T\)次詢問,每次給出線外的\(k\)個額外點的位置,顏色(黑白),和連線線內邊界格點的邊權。

要求給網格上的所有點染色,要求使得兩端顏色不同的邊權值和最小。

\(1\leq T\leq 50,\sum k\leq 50,2\leq n,m\leq 500\)

解題思路

黑白染色求最小的權值其實就是為最小割,然後平面圖最小割是可以轉換成對偶圖的最短路的。

顯然的對於\(k=2\)的部分分就是直接求黑色額外點(如果顏色都相同顯然答案為\(0\))左右的對偶點在對偶圖上的最短路。

對於\(k\)更大的情況我們具體分析一下對於下圖的情況(為了好看用了紅藍代替黑白)

我們有兩種割法(綠/黃)

發現其實可以寫成四個點相互匹配的過程,由於產生交叉的肯定不優(通過改變匹配方式使得交叉部分消去),所以匹配的貢獻可以直接寫成最短路。

然後考慮如何找到優的匹配方案,我們可以把順時針的和逆時針的匹配,因為如果順順-逆逆的匹配的話,肯定會產生交叉。(雖然這樣匹配也可能產生交叉,但是因為權值不優所以不會影響答案)

然後就是一個二分圖最大權值匹配的問題了,寫個費用流就可以了。

雖然再利用交叉性質做環形區間\(dp\)也能過,但是我不會/kk

code

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cctype>
#include<vector>
#include<queue>
#define ll long long
#define mp(x,y) make_pair(x,y)
using namespace std;
const ll N=510,M=N*N,K=110;
struct edge{
	ll to,next,w;
}a[M<<2];
struct node{
	ll w,p,t;
}q[K];
ll n,m,T,tot,ls[M],f[M],wz[N<<2];
vector<int> A,B;bool v[M];
priority_queue<pair<ll,ll> > qt;
ll read(){
	ll x=0,f=1;char c=getchar();
	while(!isdigit(c)){if(c=='-')f=-f;c=getchar();}
	while(isdigit(c)){x=(x<<1)+(x<<3)+c-'0';c=getchar();}
	return x*f;
}
struct Netflow{
	struct node{
		ll to,next,w,c;
	}a[K*K*2];
	ll tot=1,s=1,t=2,ans,ls[K],f[K],mf[K],pre[K];
	bool v[K];priority_queue<int> q;
	void clr()
	{tot=1;ans=0;memset(ls,0,sizeof(ls));return;}
	void addl(ll x,ll y,ll w,ll c){
		a[++tot].to=y;a[tot].next=ls[x];ls[x]=tot;a[tot].w=w;a[tot].c=c;
		a[++tot].to=x;a[tot].next=ls[y];ls[y]=tot;a[tot].w=0;a[tot].c=-c;
		return;
	}
	bool SPFA(){
		memset(f,0x3f,sizeof(f));
		q.push(s);f[s]=0;v[s]=1;mf[s]=1e9;
		while(!q.empty()){
			ll x=q.top();q.pop();v[x]=0;
			for(ll i=ls[x];i;i=a[i].next){
				ll y=a[i].to;
				if(a[i].w&&f[x]+a[i].c<f[y]){
					f[y]=f[x]+a[i].c;pre[y]=i;
					mf[y]=min(mf[x],a[i].w);
					if(!v[y])q.push(y),v[y]=1;
				}
			}
		}
		return (f[t]!=f[0]);
	}
	void Updata(){
		ll x=t;ans+=mf[t]*f[t];
		while(x!=s){
			a[pre[x]].w-=mf[t];
			a[pre[x]^1].w+=mf[t];
			x=a[pre[x]^1].to;
		}
		return;
	}
	ll GetAns(){
		while(SPFA())
		Updata();
		return ans;
	}
}Nt;
ll p(ll x,ll y)
{return x*(m+1)+y;}
void addl(ll x,ll y,ll w){
	a[++tot].to=y;a[tot].next=ls[x];ls[x]=tot;a[tot].w=w;
	a[++tot].to=x;a[tot].next=ls[y];ls[y]=tot;a[tot].w=w;
	return;
}
void dij(ll s){
	memset(f,0x3f,sizeof(f));
	memset(v,0,sizeof(v));
	f[s]=0;qt.push(mp(0,s));
	while(!qt.empty()){
		ll x=qt.top().second;qt.pop();
		if(v[x])continue;v[x]=1;
		for(ll i=ls[x];i;i=a[i].next){
			ll y=a[i].to;
			if(f[x]+a[i].w<f[y]){
				f[y]=f[x]+a[i].w;
				qt.push(mp(-f[y],y));
			}
		}
	}
	return;
}
ll getp(ll x,ll f){
	if(x<=m)return p(0,x+f);
	if(x<=m+n)return p(x-m+f,m);
	if(x<=2*m+n)return p(n,m-(x-m-n+f));
	return p(n-(x-2*m-n+f),0);
}
bool cmp(node x,node y)
{return x.p<y.p;}
signed main()
{
	n=read();m=read();T=read();
	for(ll i=1;i<n;i++)
		for(ll j=1;j<=m;j++){
			ll w=read();
			addl(p(i,j-1),p(i,j),w);
		}
	for(ll i=1;i<=n;i++)
		for(ll j=1;j<m;j++){
			ll w=read();
			addl(p(i-1,j),p(i,j),w);
		}
	for(ll i=1;i<=m;i++)addl(p(0,i-1),p(0,i),0),wz[i]=tot;
	for(ll i=1;i<=n;i++)addl(p(i-1,m),p(i,m),0),wz[i+m]=tot;
	for(ll i=1;i<=m;i++)addl(p(n,m-i+1),p(n,m-i),0),wz[i+n+m]=tot;
	for(ll i=1;i<=n;i++)addl(p(n-i+1,0),p(n-i,0),0),wz[i+n+2*m]=tot;
	while(T--){
		ll k=read();Nt.clr();A.clear();B.clear();
		for(ll i=1;i<=k;i++){
			q[i].w=read();q[i].p=read();q[i].t=read();
			a[wz[q[i].p]].w=a[wz[q[i].p]-1].w=q[i].w;
		}
		sort(q+1,q+1+k,cmp);q[0]=q[k];q[k+1]=q[1];
		for(ll i=1;i<=k;i++)
			if(q[i].t==1&&q[i-1].t==0)A.push_back(getp(q[i].p,-1));
		for(ll i=1;i<=k;i++)
			if(q[i].t==1&&q[i+1].t==0)B.push_back(getp(q[i].p,0));
		for(ll i=0;i<A.size();i++){
			Nt.addl(1,3+i,1,0);
			dij(A[i]);
			for(ll j=0;j<B.size();j++)
				Nt.addl(3+i,3+A.size()+j,1,f[B[j]]);
		}
		for(ll i=0;i<B.size();i++)Nt.addl(3+A.size()+i,2,1,0);
		printf("%lld\n",Nt.GetAns());
		for(ll i=1;i<=k;i++)
			a[wz[q[i].p]].w=a[wz[q[i].p]-1].w=0;
	}
	return 0;
}