CSP-S 2021
让我们先忽略廊桥数量的限制来安排航班。我们维护一个空闲的廊桥队列,每到达一架航班,就给它安排编号最小的廊桥供其使用。
现在加上廊桥数量的限制。容易发现刚才的廊桥分配方法直接就帮我们解决了廊桥限制的问题:如果当前有 \(n\) 个廊桥可供使用,则分配到 \(n+1\) 号及以后的廊桥实质上就是分配到远机位了,不需要再做任何额外的处理。
到这里做法就很清晰了:我们按照开头提到的分配方法来安排航班的停靠位置,记录各廊桥停靠的航班数,做一个前缀和,最后枚举分配给某个区的廊桥数,算出各情况下两区实际使用廊桥的航班数总和,即可解决本题。
第一篇题解写的挺好的,自己就不打了,嘿嘿。
\(code\)
#include<bits/stdc++.h>
#define N 100005
#define X first
#define Y second
#define pii pair<int,int>
using namespace std;
int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-f;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
struct node{
int l,r;
bool operator < (const node& A) const{return l==A.l?r<A.r:l<A.l;}
}a[N],b[N];
int n,m1,m2,ans;
int res1[N],res2[N];
void solve(node* t,int m,int* res){
priority_queue<pii,vector<pii>,greater<pii> > lq;
priority_queue<int,vector<int>,greater<int> > wq;
for(int i=1;i<=n;++i) wq.push(i);
for(int i=1;i<=m;++i){
while(!lq.empty()&&t[i].l>=lq.top().X){
wq.push(lq.top().Y);lq.pop();
}
if(wq.empty()) continue;
int x=wq.top();
wq.pop(),res[x]++;
lq.push(make_pair(t[i].r,x));
}
for(int i=1;i<=n;++i) res[i]+=res[i-1];
}
int main(){
n=read(),m1=read(),m2=read();
for(int i=1;i<=m1;++i) a[i].l=read(),a[i].r=read();
for(int i=1;i<=m2;++i) b[i].l=read(),b[i].r=read();
sort(a+1,a+1+m1),sort(b+1,b+1+m2);
solve(a,m1,res1),solve(b,m2,res2);
for(int i=0;i<=n;++i) ans=max(res1[i]+res2[n-i],ans);
printf("%d\n",ans);
return 0;
}
首先肯定是区间dp,令 \(dp_{i,j}\) 表示从位置 \(i\) 到位置 \(j\) 一共的合法序列总情况数量。
但是不同的形态可能会有不同的转移,如:(S)这种只能从S转移过来等等。所以只开两维的dp状态必然是不够的。
可以将两位的dp扩充为三维,第三位表示不同的形态种类,dp状态就变成了 \(dp_{i,j,[0,5]}\)。没种状态表示:
-
\(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}\): 形态如
***(...)**(...)**
的括号序列(即左边以*
开头,右边以*结尾,注意:第1种形态也属于这种形态)。
设定完状态以后,转移就直接出来了,注意:为了防止连续超过 \(k\) 个*
一起出现,转移的时候不能把两段*
拼接起来,在状态1的时候暴力判断一下两端的距离是否是 \(\le k\) 的,是的才能转移。
-
\(dp_{l,r,0}\)(直接特判)
没什么好解释的 -
$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})\times compare(l,r) $,
$compare(i,j) $ 表示第 \(i\) 位与第 \(j\) 位能否配对成括号,能则为 \(1\),否则为 \(0\)。
加括号时,里面可以是全*
,可以是有一边是*
, 也可以是两边都不是*
,唯独不能两边都是*
且中间有括号序列。 -
\(dp_{l,r,2}=\sum\limits_{i=l}^{r-1} dp_{l,i,3}\times dp_{i+1,r,0}\)
左边以括号序列开头且以括号序列结尾的是第3种,右边接一串*
,是第0种。 -
\(dp_{l,r,3}=\sum\limits_{i=l}^{r-1} (dp_{l,i,2}+dp_{l,i,3})\times dp_{i+1,r,1}+dp_{l,r,1}\)
左边以括号序列开头,结尾随便,符合的有第2和第3种,右边接一个括号序列,是第1种。
记得加上直接一个括号序列的。 -
\(dp_{l,r,4}=\sum\limits_{i=l}^{r-1} (dp_{l,i,4}+dp_{l,i,5})\times dp_{i+1,r,1}\)
左边以*
开头,结尾随便,符合的有第4和第5种,右边接一个括号序列,是第1种。 -
\(dp_{l,r,4}\) 还有一种递推式也是可以的 , \(dp_{l,r,4}=\sum\limits_{i=l}^{r-1} (dp_{l,i,0}\times dp_{i+1,r,3})\)(亲测有效,可见两次提交记录 link1,link2)
-
\(dp_{l,r,5}=\sum\limits_{i=l}^{r-1} dp_{l,i,4}\times dp_{i+1,r,0}+dp_{l,r,0}\)
左边以*
开头,以括号序列结尾,符合的是第4种,右边接一串*
,是第0种。
记得加上全是*
的。
最后,答案必须以括号序列开头,以括号序列结尾,所以直接是 \(dp_{1,n,3}\)。
这样,初始状态也就没什么问题了,对于所有的 \(i\) 满足 \(1\le i \le n\),有 \(dp_{i,i-1,0}=1\) 。
最终时间复杂度 \(O(6\times n^3)\) 不到,(后半部分填不满 \(n^3\) )。
记得开long long,并且取模。
$code $
#include<bits/stdc++.h>
#define N 505
#define mod 1000000007
#define int long long
using namespace std;
int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-f;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
int n,k;
char s[N];
int f[N][N][6];
// f 0 ***
// f 1 ()
// f 2 ()***
// f 3 ()**() 包含 f 1
// f 4 ***()**()
// f 5 ***()**()*** 包含 f 0
bool check(int a,int b){return (s[a]=='('||s[a]=='?')&&(s[b]==')'||s[b]=='?');}
signed main(){
n=read(),k=read();
scanf("%s",s+1);
for(int i=1;i<=n;++i) f[i][i-1][0]=1;
for(int l=1;l<=n;++l){
for(int i=1;i+l-1<=n;++i){
int j=i+l-1;
if(l<=k) f[i][j][0]=f[i][j-1][0]&&(s[j]=='?'||s[j]=='*');
if(l>=2){
if(check(i,j)) f[i][j][1]=(f[i+1][j-1][0]+f[i+1][j-1][2]+f[i+1][j-1][3]+f[i+1][j-1][4])%mod;
for(int k=i;k<j;++k){
f[i][j][2]=(f[i][j][2]+f[i][k][3]*f[k+1][j][0])%mod;
f[i][j][3]=(f[i][j][3]+(f[i][k][2]+f[i][k][3])*f[k+1][j][1])%mod;
// f[i][j][4]=(f[i][j][4]+(f[i][k][4]+f[i][k][5])*f[k+1][j][1])%mod; //两种均可
f[i][j][4]=(f[i][j][4]+f[i][k][0]*f[k+1][j][3])%mod;
f[i][j][5]=(f[i][j][5]+f[i][k][4]*f[k+1][j][0])%mod;
}
}
f[i][j][5]=(f[i][j][5]+f[i][j][0])%mod;
f[i][j][3]=(f[i][j][3]+f[i][j][1])%mod;
}
}
printf("%lld\n",f[1][n][3]);
return 0;
}
枚举 \(b\) 中开头的值(显然它只能是 \(a\) 的开头和末位),然后在 \(a\) 中找到与之对应的值 (即 \(b\) 的末位),这就相当于一个区间往里面缩,一个区间往外拓展,这样搜索的复杂度是 \(O(2^{n/2})\)。
考虑对其再来一个贪心,显然优先级为 \(LL --> LR --> RL --> RR\),只要有一个可行我们就只搜这一条路,那么复杂度为 \(O(n)\) (每次只移动一次,每移动一次数组长度 \(-2\) )。
\(code\)
#include<bits/stdc++.h>
#define N 1000005
using namespace std;
int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-f;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
int T,n;
char ans[N];
int a[N],b[N],L[N],R[N];
void init(){
memset(b,0,sizeof(b));
memset(ans,0,sizeof(ans));
memset(L,0,sizeof(L));
memset(R,0,sizeof(R));
}
bool solve(int lst,int Li,int Ri){
if(Li==2) ans[1]='L';
else ans[1]='R';
int l=lst,r=lst,bg=2,ed=n-1;
for(int i=1;i<n/2;++i){
if(R[a[Li]]==l-1) l--,Li++,ans[bg++]='L',ans[ed--]='L';
else if(R[a[Li]]==r+1) r++,Li++,ans[bg++]='L',ans[ed--]='R';
else if(L[a[Ri]]==l-1) l--,Ri--,ans[bg++]='R',ans[ed--]='L';
else if(L[a[Ri]]==r+1) r++,Ri--,ans[bg++]='R',ans[ed--]='R';
else return 0;
}
ans[n]='L';
for(int i=1;i<=n;++i) printf("%c",ans[i]);printf("\n");
return 1;
}
signed main(){
T=read();
while(T--){
init();
n=read()*2;
for(int i=1;i<=n;++i) a[i]=read(),L[a[i]]?R[a[i]]=i:L[a[i]]=i;
if(solve(R[a[1]],2,n)) continue;
if(solve(L[a[n]],1,n-1)) continue;
printf("-1\n");
}
return 0;
}