[题解]NOIP 2024 T1~T2
编辑字符串(edit)
初见感觉像贪心,但在不好写+不会证的情况下放弃了,然后想到DP又设不出状态。
实际上……就是贪心哦?
下文称\(s_1,s_2\)分别为\(a,b\)。
不难发现一个不存在锁定位置的区间,其内元素是可以任意交换的。
所以我们可以按照锁定位置,将两个字符串划分成若干个区间(被锁定的位置可以看作单独的一个区间)。
贪心的思路,就是一个指针\(i\)从头开始遍历,不断取\(a_i,b_i\)所在的区间,如果两个区间都存在未被匹配的\(0\),就用\(0\)进行匹配,否则如果存在未被匹配的\(1\),就用\(1\)匹配。
正确性仔细一想其实比较显然。
我们各取\(a,b\)最左边的区间\(A\)和\(B\)(显然它们是左端对齐的),假设\(|A|\ge |B|\)。
按贪心对两个区间进行匹配后,如果将\(A\)长度为\(|B|\)的前缀中任选一个元素交换到此前缀之后:
- 如果交换出去后,该前缀对答案的贡献\(-1\):因为交换出去的那个元素最多能产生\(+1\)的贡献,所以最好情况下也只能使答案不变。
- 如果交换出去后,该前缀对答案的贡献不变:说明交换的两个值相等,所以答案不变。
- 如果交换出去后,该前缀对答案的贡献\(+1\):根据贪心的过程,一定不存在这种情况。
点击查看代码
#include<bits/stdc++.h>
#define N 100010
using namespace std;
int t,n,pa[N],pb[N],ca[N][2],cb[N][2],ans;
string a,b,c,d;
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr),cout.tie(nullptr);
cin>>t;
while(t--){
memset(ca,0,sizeof ca);
memset(cb,0,sizeof cb);
cin>>n>>a>>b>>c>>d;
for(int i=0,p=0;i<n;i++){
if(c[i]&1) pa[i]=((~p)?p:(p=i));
else pa[i]=i,p=-1;
}
for(int i=0,p=0;i<n;i++){
if(d[i]&1) pb[i]=((~p)?p:(p=i));
else pb[i]=i,p=-1;
}
for(int i=0;i<n;i++)
ca[pa[i]][a[i]&1]++,cb[pb[i]][b[i]&1]++;
ans=0;
for(int i=0;i<n;i++){
if(ca[pa[i]][0]&&cb[pb[i]][0])
ans++,ca[pa[i]][0]--,cb[pb[i]][0]--;
else if(ca[pa[i]][1]&&cb[pb[i]][1])
ans++,ca[pa[i]][1]--,cb[pb[i]][1]--;
}
cout<<ans<<"\n";
}
return 0;
}
遗失的赋值(assign)
如果给定的一元关系本就存在矛盾,特判输出\(0\)即可。我们只分析一元关系不存在矛盾的情况。
一种二元关系的构造是不合法的,当且仅当存在二元关系区间\([l,r)\),满足:
- \(x_l,x_r\)的值均是一元关系给定的;
- \(a_l=x_l\);
- 对于\(i\in(l,r)\),都有\(a_i=b_{i-1}\);
- \(b_{r-1}\ne x_r\)。
进一步我们发现,将第\(1\)条(条件A)替换成“\([l,r]\)中 仅有 \(x_l,x_r\)的值是一元关系给定的”(条件B),这样判定仍然是正确的。因为每一个满足条件A的不合法的大区间\([l,r]\),都包含同样满足条件B的不合法的小区间\([l',r']\)。
比如下图,红色线段是二元限制,绿点是一元限制。\([12,16)\)这个满足条件A的不合法区间,就包含\([14,16)\)这个满足条件B的不合法区间。
所以我们可以将一元关系从小到大排序,依次考虑相邻的一元关系之间,合法的构造个数,用乘法原理计算答案即可。
对于\(x_l,x_r\)两个相邻的一元关系,它们之间合法的构造个数就是总个数\(-\)不合法的构造个数。
- \(l,r\)之间有\(r-l\)个二元关系,每个二元关系有\(2\)个值,所以总个数是\(v^{2(r-l)}\)。
- 根据上面的判定条件,我们要让\([l,r)\)之间的二元关系连成一条链,其中左端点只能选\(x_l\),中间的点都可以随便选择,右端点可以选除了\(x_r\)以外的任何值。所以不合法的构造数是\(v^{r-l-1}\times (v-1)\)。
相减,得到\(v^{2(r-l)}-v^{r-l-1}\times (v-1)\)。用所有\(l,r\)的答案累乘起来就可以了(所以答案其实是和\(d\)无关的)。
注意,还需要额外累乘最左边和最右边的答案,显然这两处都可以随便构造,所以直接累乘合法的数量即可。
每组测试数据\(O(m(\log m+\log n))\)。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define mod 1000000007
#define M 100010
using namespace std;
struct Limit{int c,d;}lim[M];
int t,n,m,v;
int qpow(int a,int n){
int ans=1;
while(n){
if(n&1) ans=ans*a%mod;
a=a*a%mod,n>>=1;
}
return ans;
}
int solve(){
sort(lim+1,lim+1+m,[](Limit a,Limit b){return a.c<b.c;});
for(int i=1;i<m;i++)
if(lim[i].c==lim[i+1].c&&lim[i].d!=lim[i+1].d) return 0;
int ans=1;
for(int i=1;i<m;i++){
int l=lim[i].c,r=lim[i+1].c;
if(l==r) continue;
ans=ans*((qpow(v,(r-l)<<1)-qpow(v,r-l-1)*(v-1))%mod+mod)%mod;
}
ans=ans*qpow(v,(lim[1].c-1)<<1)%mod;
ans=ans*qpow(v,(n-lim[m].c)<<1)%mod;
return ans;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr),cout.tie(nullptr);
cin>>t;
while(t--){
cin>>n>>m>>v;
for(int i=1;i<=m;i++) cin>>lim[i].c>>lim[i].d;
cout<<solve()<<"\n";
}
return 0;
}
树的遍历(traverse)
我们将边看作点,构建一个新图。新图上的两点之间有边,当且仅当它们对应原图上的两边有公共顶点。这样构建出的图称为原图的线图,这道题就是指定了线图上的若干个关键点,让我们求出从这些关键点开始遍历能得到的DFS树的形态数。
\(k=1\)的情况比较送,因为不难发现原图上某点的邻接边,在线图上构成了一个团(完全子图)。一个完全图的DFS树是一条链,由于原图是一棵树,所以指定DFS树的根节点后,每个团第一个被遍历到的点是确定的,而其他点顺序任意。所以答案是\(\prod\limits_{u\in {V}} (deg_u-1)!\),其中\(V\)是原图上的点集,\(deg_u\)是\(u\)的度数。
正解之后补。