CSP-S2021复赛题解

T1 廊桥分配

题意:给两组线段,分别 m 1 m1 m1 m 2 m2 m2 个,共计 n n n 条直线,要求每组内线段起点靠前的必须优先放置,同一条直线上的线段互不相交,问如何分配这 n n n 条直线使线段放置的总数最多

首先 O O O( n 2 l o g n n^2logn n2logn)的做法很显然,枚举分配给 x x x 1 − n 1-n 1n 个廊桥, y y y 组相应的用 n n n 减,然后模拟飞机进廊桥算总和取 m a x max max 即可

考虑怎么优化

挖掘题目性质:根据“先到先得”的原则,只要来了一个飞机,只要还有空余的廊桥,就必须把它放进去,所以当这一组廊桥数目增多时,原来在哪个廊桥的飞机仍然在哪个廊桥

(重点部分:)所以,如果强制让它进入编号最小的廊桥,这样不会影响进入廊桥飞机的集合。这样的话,我们在每个编号为 i i i 的廊桥上记录可以放多少个飞机 c c c[ i i i] ,那么给该组 a a a 个廊桥时,放的飞机总数就是 c c c[ 1 1 1] 到 c c c[ a a a] 的和,即前缀和。这样预处理,我们成功将外层枚举分配廊桥个数的复杂度降下来了。现在复杂度为 O O O( n l o g n nlogn nlogn)

最后说一下用小根堆来模拟飞机进廊桥的过程,开一个堆 s s s ,表示现在可用的廊桥编号,堆 q q q 表示在廊桥内的飞机的结束时间和其所在的廊桥编号(用个 p a i r pair pair 打包即可)。具体实现:我们钦定来一个飞机先进入编号小的廊桥,对于一个新来的飞机 x x x ,先把所有已占廊桥中的飞机的结束时刻比 x x x 到达时刻小的 p o p pop pop 掉,然后若还有剩余廊桥,就把 x x x 的结束时刻捆上当前空余廊桥的最小编号扔进堆里。

#include 
using namespace std;
int n,m1,m2,ans,cx[100005],cy[100005];
struct node{int s,t;}x[100005],y[100005];
bool cmp(node a,node b){return a.s<b.s;}
priority_queue<int,vector<int>,greater<int> >s;
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > >q;
int main(){
	scanf("%d%d%d",&n,&m1,&m2);
	for(int i=1;i<=m1;i++)scanf("%d%d",&x[i].s,&x[i].t);
	for(int i=1;i<=m2;i++)scanf("%d%d",&y[i].s,&y[i].t);
	sort(x+1,x+1+m1,cmp);sort(y+1,y+1+m2,cmp);
	for(int i=1;i<=n;i++)s.push(i);
	for(int i=1;i<=m1;i++){
		if(!q.empty()){
			while(!q.empty()&&x[i].s>q.top().first)
				s.push(q.top().second),q.pop();
		}
		if(!s.empty()){
			int k=s.top();s.pop();
			cx[k]++;q.push(make_pair(x[i].t,k));
		}
	}
	for(int i=1;i<=n;i++)cx[i]+=cx[i-1];
	while(!s.empty())s.pop();while(!q.empty())q.pop();
	for(int i=1;i<=n;i++)s.push(i);
	for(int i=1;i<=m2;i++){
		if(!q.empty()){
			while(!q.empty()&&y[i].s>q.top().first)
				s.push(q.top().second),q.pop();
		}
		if(!s.empty()){
			int k=s.top();s.pop();
			cy[k]++;q.push(make_pair(y[i].t,k));
		}
	}
	for(int i=1;i<=n;i++)cy[i]+=cy[i-1];
	for(int i=0;i<=n;i++)ans=max(ans,cx[i]+cy[n-i]);
	printf("%d\n",ans);
	return 0;
}

T2 括号序列

根据题目描述,符合要求的括号序列可以分为两类
1.包含型:(),(A),(S),(AS),(SA)
2.并列型:AA,ASA
其中 S 表示任意一个仅由不超过k 个字符 * 组成的非空字符串

区间dp,设dp[i,j,k]表示从位置i到位置j一共合法序列的总情况数,k表示不同的形态种类

  • dp[i][j][0]:形如 ***** 的括号序列(即全部是*)
  • dp[i][j][1]:形如 (…) 的括号序列(即左右直接被括号包裹且最左边括号与最右边括号相互匹配)
  • dp[i][j][2]:形如 (...)**(...)*** 的括号序列(即左边以括号序列开头,右边以*结尾)
  • dp[i][j][3]:形如(...)***(...)*(...)的括号序列(即左边以括号序列开头,右边以括号序列结尾,注:第2种形态也属于这种形态)
  • dp[i][j][4]:形如***(...)**(...)的括号序列(即左边以*开头,右边以括号序列结尾)
  • dp[i][j][5]:形如***(...)**(...)**的括号序列(即左边以*开头,右边以*结尾,注:第一种形态也属于这种形态)
#include 
#define int long long
using namespace std;
const int mod=1e9+7;
int n,k,dp[505][505][6];
char s[505];
bool check(int a,int b){return (s[a]=='('||s[a]=='?')&&(s[b]==')'||s[b]=='?');}
signed main(){
    scanf("%lld%lld",&n,&k);scanf("%s",s+1);
    for(int i=1;i<=n;i++)dp[i][i-1][0]=1;
    for(int len=1;len<=n;len++){
        for(int l=1;l+len-1<=n;l++){
            int r=l+len-1;
            if(len<=k)dp[l][r][0]=dp[l][r-1][0]&&(s[r]=='*'||s[r]=='?');
            if(len>=2){
                if(check(l,r)) dp[l][r][1]=(dp[l+1][r-1][0]+dp[l+1][r-1][2]+dp[l+1][r-1][3]+dp[l+1][r-1][4])%mod;
                for(int i=l;i<=r-1;i++){
                    dp[l][r][2]=(dp[l][r][2]+dp[l][i][3]*dp[i+1][r][0])%mod;
                    dp[l][r][3]=(dp[l][r][3]+(dp[l][i][2]+dp[l][i][3])*dp[i+1][r][1])%mod;
                    dp[l][r][4]=(dp[l][r][4]+(dp[l][i][4]+dp[l][i][5])*dp[i+1][r][1])%mod;
                    dp[l][r][5]=(dp[l][r][5]+dp[l][i][4]*dp[i+1][r][0])%mod;
                }
            }
            dp[l][r][5]=(dp[l][r][5]+dp[l][r][0])%mod;
            dp[l][r][3]=(dp[l][r][3]+dp[l][r][1])%mod;
        }
    }
    printf("%lld\n",dp[1][n][3]);
}

T3 回文

题意:给定一个正整数序列 a a a 和一个空序列 b b b ,每次可以从 a a a 的左端或右端取出一个正整数放入 b b b 的左端,并分别记为 L L L R R R ,求使 b b b 成为回文串且操作序列字典序最小的操作方案。

首先最开始一定取 a 1 a_1 a1 a n a_n an ,以取 a 1 a_1 a1 为例:找到与 a 1 a_1 a1 相同的唯一一个位置记作 p p p ,则序列被划分为两部分,可以看作两个栈, S 1 S_1 S1 : a [ 2... p − 1 ] a[2...p-1] a[2...p1] 的栈顶是 2 2 2,栈底是 p − 1 p-1 p1 S 2 S_2 S2 a [ p + 1..2 n ] a[p+1..2n] a[p+1..2n] 的栈顶为 2 n 2n 2n,栈底为 p + 1 p+1 p+1,则问题转化为每次可以取走这两个栈之一的栈顶,令最终的到的串是回文串

考虑最终得到的是一个回文串,可知第 i i i 次和第 2 n − i + 1 2n-i+1 2ni+1 次取出的数应相同,所以可以同时分析并处理第 i i i 次与第 2 n − i + 1 2n-i+1 2ni+1 次操作,即需要同时处理两部分中当前最先取出与最后被处理的数(可以在同一部分取,也可以在不同部分取),也就是两部分分别的两端。

考虑用双端队列存储这两个部分,从出序列方向的正向出队列代表第 i i i 次操作,从出序列方向的反向出队列代表第 2 n − i + 1 2n-i+1 2ni+1 次操作。第 i i i 次操作按照优先级有如下 4 种情况:

  • L的左端等于L的右端,则L的左端与L的右端出队列,第 i i i 次操作为L,第 2 n − i + 1 2n-i+1 2ni+1 次操作为L
  • L的左端等于R的左端,则L的左端与R的左端出队列,第 i i i 次操作为L,第 2 n − i + 1 2n-i+1 2ni+1 次操作为R
  • R的右端等于L的右端,则R的右端与L的右端出队列,第 i i i 次操作为R,第 2 n − i + 1 2n-i+1 2ni+1 次操作为L
  • R的右端等于R的左端,则R的右端与R的左端出队列,第 I I I 次操作为R,第 2 n − i + 1 2n-i+1 2ni+1 次操作为R

则对于每次操作我们只需按照优先级进行判断,优先执行高优先级的操作,不断重复,如果有哪次操作一种都执行不了,则输出-1,否则一直执行到结束位置,最终输出生成的操作序列即可,复杂度 O ( n ) O(n) O(n)

#include 
using namespace std;
int t,n,a[1000005];
char s[1000005];
inline bool work(int l1,int r1,int l2,int r2){
	for(int i=1;i<=n-1;i++){
		if(l1<=r1&&((l2<=r2&&a[l1]==a[l2])||(l1<r1&&a[l1]==a[r1]))){
			if(l1<r1&&a[l1]==a[r1]){
				l1++;r1--;s[i]='L';s[2*(n-1)-i+1]='L';
			}else{
				l1++;l2++;s[i]='L';s[2*(n-1)-i+1]='R';
			}
		}
		else if(l2<=r2&&((l1<=r1&&a[r2]==a[r1])||(l2<r2&&a[l2]==a[r2]))){
			if(l2<r2&&a[l2]==a[r2]){
				l2++;r2--;s[i]='R';s[2*(n-1)-i+1]='R';
			}
			else {
				r2--;r1--;s[i]='R';s[2*(n-1)-i+1]='L';
			}
		}
		else return 0;
	}
	return 1;
}
int main(){
	scanf("%d",&t);
	while(t--){
		scanf("%d",&n);int p1=-1,p2=-1;
		for(int i=1;i<=n*2;i++)scanf("%d",&a[i]);
		for(int i=1;i<=n*2+1;i++)s[i]=0;
		for(int i=2;i<=n*2;i++){if(a[1]==a[i]){p1=i;break;}}
		for(int i=1;i<=n*2-1;i++){if(a[2*n]==a[i]){p2=i;break;}}
		if(work(2,p1-1,p1+1,n*2)){printf("L%sL\n",s+1);}
		else if(work(1,p2-1,p2+1,n*2-1)){printf("R%sL\n",s+1);}
		else printf("-1\n");
	}
	return 0;
}

你可能感兴趣的:(算法,贪心算法)