[题解]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\)的度数。
正解之后补。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效