题面

[传送门](https://arc083.contest.atcoder.jp/tasks/arc083_d)

思路

这是一道真正的好题

第一步:转化模型

行列支配类的问题,常见做法就是把行和列变成二分图中的点,把矩阵内元素作为边,转化为图论问题

本题中,我们把第$(i,j)$格子中的球,变成连接$i$行和$j$列的无向边即可

容易发现,对于不同的联通块之间,子问题互相没有影响,因此可以对于每个块分别处理

第二步:联通块性质

观察可得,若任何一个联通块的点数和边数不相等,那么题目无解

若满足这个条件,显然所有的联通块都是环套树的形态

再次考虑本题的原来意义,可以发现,我们就是要对每一个点,找一条和它相邻的边,让这个点支配这条边

那么在环套树意义下,显然环上的边只能由环上点来支配,树上边只能由非环点来支配

这样,问题的解即为树上的点支配它连向父亲的边,而环上点依次顺时针或者逆时针支配环上的边

每个联通块有且只有两个解

考虑上述解,显然可以把“点支配边”的过程理解为给边重定向,也就是说这个过程之后我们得到了一个基环内向树,环内边可能是顺时针或者逆时针

第三步:判断解的操作顺序及其合法性

考虑题目中的一个限制:如果$u$行的点想要支配$(u,v)$位置的小球,那么所有位于$(u,w)(1\leq w < v)$的小球都必须先被其他$w$列支配才行

发现这个限制,如果把整个联通块的限制放到一起,会形成一个DAG,表示支配关系

同时易证这个DAG一定是一棵树或者一个森林(支配关系不能被同步影响再同步影响其它点)

那么,设对于点$u$而言,无向环套树上与它相连的点为${v_1,v_2,v_3...v_k}$,这个点集是从小到大排好序的

如果点$u$选择支配边$(u,v_i)$,那么我们应当在支配DAG上,连边$(v_j,u)(1\leq j < i)$

这样,我们会得到一个支配森林,森林中的边是从儿子到父亲的有向DAG边

第四步:计算答案

对于一棵支配树,显然这个支配树的问题来源的那棵环套树的答案,就是这棵树的拓扑序数量

树的拓扑序数量计算方式为$Ans=\frac{siz(root)!}{\prod siz(u)}$,其中$root$为根,$u$为任意节点,结论证明点这里

对于一个森林,计算它的拓扑序数量如下:

$Ans=\frac{(\sum siz(root_i))!}{\prod siz(u)}$,其中$root_i$为森林中树的根,$u$为任意节点,证明同上

本题中,如果我们确定了每个联通块的环套树的环上定向方向,那么所有联通块的基环内向树的生成支配森林构成的大森林的拓扑序数量即为这种定向方案的答案

但是,如果用2的联通块个数次方种定向方式来加起来计算答案,显然时间上难以接受

因此我们考虑把这一步加法原理放到中间过程中处理

第五步:优化计算

考虑到这个加法原理的本质,是每个联通块可以有两种不同的生成方式

那么我们只需要把加法原理挪到这里即可,最终的答案计算方法如下:

对于某一个联通块的基环内向树生成的两种森林(分别对应两种环上定向方式),求出森林的拓扑序数量的分母,记为$A$

注意这里相当于求$A=\frac{1}{\prod siz(u)}$,其中$root_i$为森林中树的根,$u$为任意节点

然后,对于一个联通块,把两个$A$相加得到$B$

对于总问题,把每个联通块的$B$乘起来,再乘以$2n!$,就得到了答案

这个过程实际上是把森林到联通块之间构成的总森林的过程,视为树到森林的拓扑序数量计算合并过程

中间用了一步加法原理

总结

遇到Atcoder题,想不出来的时候,请试一试建图

如果建图成功但是做不出来,想想环套树吧

(上面两条是玄学)

毋庸置疑,这是一道好题,题目中一共有了3个问题转化,以及两处子问题分治,子问题和模型之间也有互相的影响、结论

解决这类题目的关键,在于及时把握好子模型在原问题中的意义,适时引入原问题结论,得到子问题结论

但是同时也要注意不能太过依赖于原模型的结论,毕竟新建立的子模型,也具有模型本身的一些结论可以加以利用

Code

拒绝压行,从我做起

  1. #include<iostream>
  2. #include<cstdio>
  3. #include<cstring>
  4. #include<algorithm>
  5. #include<cassert>
  6. #include<stack>
  7. #include<vector>
  8. #define MOD 1000000007
  9. #define ll long long
  10. using namespace std;
  11. inline int read(){
  12. int re=0,flag=1;;char ch=getchar();
  13. while(!isdigit(ch)){
  14. if(ch=='-') flag=-1;
  15. ch=getchar();
  16. }
  17. while(isdigit(ch)) re=(re<<1)+(re<<3)+ch-'0',ch=getchar();
  18. return re*flag;
  19. }
  20. ll inv[200010],fac[200010];
  21. namespace topo{
  22. int first[200010],cnte;
  23. vector<int>e[200010];
  24. int inbound[200010],worked[200010];
  25. vector<int>appearance;
  26. inline void add(int u,int v){
  27. e[u].push_back(v);
  28. inbound[v]++;
  29. }
  30. int siz[200010];
  31. ll dfs(int u,int f){
  32. ll re=1;siz[u]=1;
  33. assert(u);
  34. for(auto v:e[u]){
  35. if(v==f) continue;
  36. (re*=dfs(v,u))%=MOD;
  37. siz[u]+=siz[v];
  38. }
  39. return re*inv[siz[u]]%MOD;
  40. }
  41. ll solve(){
  42. ll re=1,tmp;
  43. for(auto u:appearance){
  44. if(!inbound[u]&&!worked[u]){
  45. tmp=dfs(u,0);
  46. worked[u]=1;
  47. (re*=tmp%MOD)%=MOD;
  48. }
  49. }
  50. for(auto u:appearance){
  51. e[u].clear();
  52. inbound[u]=0;
  53. worked[u]=0;
  54. }
  55. return re;
  56. }
  57. }
  58. vector<int>e[200010];
  59. stack<int>s;
  60. bool vis[200010],on_circle[200010];int cnt1,cnt2,lped;
  61. vector<int>circle;
  62. void dfs(int u,int f){
  63. int i,v;cnt1++;s.push(u);
  64. vis[u]=1;
  65. for(i=0;i<e[u].size();i++){
  66. v=e[u][i];if(v==f||on_circle[v]) continue;
  67. cnt2++;
  68. if(!lped&&vis[v]){
  69. lped=1;
  70. while(s.top()!=v){
  71. circle.push_back(s.top());
  72. on_circle[s.top()]=1;
  73. s.pop();
  74. }
  75. circle.push_back(s.top());
  76. on_circle[s.top()]=1;
  77. s.pop();
  78. }
  79. else if(!vis[v]){
  80. dfs(v,u);
  81. }
  82. }
  83. if(!s.empty()&&s.top()==u) s.pop();
  84. }
  85. int control[200010];
  86. void dfs2(int u,int f){
  87. int i,v;
  88. for(i=0;i<e[u].size();i++){
  89. v=e[u][i];
  90. if(v==f||on_circle[v]) continue;
  91. control[v]=u;
  92. dfs2(v,u);
  93. }
  94. }
  95. void adde(int u,int f){
  96. topo::appearance.push_back(u);
  97. for(auto v:e[u]){
  98. if(v==control[u]) break;
  99. topo::add(u,v);
  100. }
  101. for(auto v:e[u]){
  102. if(v==f||on_circle[v]) continue;
  103. adde(v,u);
  104. }
  105. }
  106. ll solve(int u){
  107. cnt1=cnt2=lped=0;
  108. dfs(u,0);
  109. if(cnt1!=cnt2) return 0;
  110. int i;ll re=0;
  111. for(i=0;i<circle.size();i++){
  112. dfs2(circle[i],0);
  113. }
  114. //first
  115. for(i=0;i<circle.size();i++){
  116. control[circle[i]]=circle[(i+1)%circle.size()];
  117. }
  118. for(i=0;i<circle.size();i++){
  119. adde(circle[i],0);
  120. }
  121. re+=topo::solve();
  122. topo::appearance.clear();
  123. //second
  124. for(i=0;i<circle.size();i++){
  125. control[circle[i]]=circle[(i-1+circle.size())%circle.size()];
  126. }
  127. for(i=0;i<circle.size();i++){
  128. adde(circle[i],0);
  129. }
  130. re+=topo::solve();
  131. topo::appearance.clear();
  132. for(i=0;i<circle.size();i++) on_circle[i]=0;
  133. circle.clear();
  134. return re%MOD;
  135. }
  136. int n;
  137. int main(){
  138. n=read();int i,t1,t2;
  139. memset(topo::first,-1,sizeof(topo::first));
  140. inv[1]=1;fac[1]=1;
  141. for(i=2;i<=n*2;i++) inv[i]=(MOD-MOD/i)*inv[MOD%i]%MOD;
  142. for(i=2;i<=n*2;i++) fac[i]=fac[i-1]*i%MOD;
  143. for(i=1;i<=n*2;i++){
  144. t1=read();t2=read();t2+=n;
  145. e[t1].push_back(t2);
  146. e[t2].push_back(t1);
  147. }
  148. for(i=1;i<=n*2;i++){
  149. if(e[i].size()) sort(e[i].begin(),e[i].end());
  150. }
  151. ll ans=1;
  152. for(i=1;i<=n*2;i++){
  153. if(!vis[i]) (ans=ans*solve(i))%=MOD;
  154. }
  155. cout<<(fac[2*n]*ans)%MOD<<'\n';
  156. }

[ARC083F] Collecting Balls [建二分图+环套树定向+建拓扑图+树的拓扑序计数]的更多相关文章

  1. 题解-AtCoder ARC-083F Collecting Balls

    Problem ARC083F 题意概要:给定 \(2n\) 个二维平面上的球,坐标分别为 \((x_i,y_i)\),并给出 \(n\) 个 \(A\)类 机器人 和 \(n\) 个 \(B\)类 ...

  2. Arc083_F Collecting Balls

    传送门 题目大意 给定$N$,在$(1,0),(2,0)......(N,0)$和$(0,1),(0,2)...(0,N)$上都有$1$个机器人,同时给定$2N$个坐标$(x,y),x,y\in[1, ...

  3. Codeforces 1045. A. Last chance(网络流 + 线段树优化建边)

    题意 给你 \(n\) 个武器,\(m\) 个敌人,问你最多消灭多少个敌人,并输出方案. 总共有三种武器. SQL 火箭 - 能消灭给你集合中的一个敌人 \(\sum |S| \le 100000\) ...

  4. HDU 5669 线段树优化建图+分层图最短路

    用线段树维护建图,即把用线段树把每个区间都标号了,Tree1中子节点有到达父节点的单向边,Tree2中父节点有到达子节点的单向边. 每次将源插入Tree1,汇插入Tree2,中间用临时节点相连.那么T ...

  5. UOJ#77. A+B Problem [可持久化线段树优化建边 最小割]

    UOJ#77. A+B Problem 题意:自己看 接触过线段树优化建图后思路不难想,细节要处理好 乱建图无果后想到最小割 白色和黑色只能选一个,割掉一个就行了 之前选白色必须额外割掉一个p[i], ...

  6. CF786B Legacy(线段树优化建图)

    嘟嘟嘟 省选Day1T2不仅考了字符串,还考了线段树优化建图.当时不会,现在赶快学一下. 线段树能优化的图就是像这道题一样,一个点像一个区间的点连边,或一个区间像一个点连边.一个个连就是\(O(n ^ ...

  7. [十二省联考2019]字符串问题——后缀自动机+parent树优化建图+拓扑序DP+倍增

    题目链接: [十二省联考2019]字符串问题 首先考虑最暴力的做法就是对于每个$B$串存一下它是哪些$A$串的前缀,然后按每组支配关系连边,做一遍拓扑序DP即可. 但即使忽略判断前缀的时间,光是连边的 ...

  8. BZOJ5017 [SNOI2017]炸弹 - 线段树优化建图+Tarjan

    Solution 一个点向一个区间内的所有点连边, 可以用线段树优化建图来优化 : 前置技能传送门 然后就得到一个有向图, 一个联通块内的炸弹可以互相引爆, 所以进行缩点变成$DAG$ 然后拓扑排序. ...

  9. bzoj5017 炸弹 (线段树优化建图+tarjan+拓扑序dp)

    直接建图边数太多,用线段树优化一下 然后缩点,记下来每个点里有多少个炸弹 然后按拓扑序反向dp一下就行了 #include<bits/stdc++.h> #define pa pair&l ...

随机推荐

  1. 关于ACL中通配符掩码(反掩码)认识

    ACL(Access Control List)  访问控制列表在作为数据包的过滤器以及在对指定的某种类型的数据包的优先级,起到了对某些数据包的优先级起到了限制流量的作用,减少了网络的拥塞.      ...

  2. js实现二分查找

    二分查找需要数组是有序的,1.先从有序数组的最中间元素开始查找,如果和要查找的元素相等,直接返回索引,若不相等则下一步.2.如果指定的元素大于或者小于中间元素,则在大于或小于的那一半区域内查找,重复第 ...

  3. POJ2154 Color(Polya定理)

    Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 11654   Accepted: 3756 Description Bead ...

  4. HTML5一些标签和属性

    <bdo> 元素 可以覆盖默认文本的方向  根据dir 属性来控制文字的排序方向            属性:dir="rtl"                     ...

  5. Linux nohup 关闭终端的时候,程序依然能在后台运行( linux重定向及nohup不输出的方法)

    先说一下linux重定向: 0.1和2分别表示标准输入.标准输出和标准错误信息输出,可以用来指定需要重定向的标准输入或输出.在一般使用时,默认的是标准输出,既1.当我们需要特殊用途时,可以使用其他标号 ...

  6. Ansible工作架构和原理

    特性 模块块化调用持定的模块,完成持定任务 有Paramiko,PyYAML,Jinja2(模板语言)三个关键模块 支持自定义模块 基于Python语法头现 部署简单,基于python和SSH(默认已 ...

  7. python核心编程2 第五章 练习

    5-2 运算符(a) 写一个函数,计算并返回两个数的乘积(b) 写一段代码调用这个函数,并显示它的结果 def product(x, y): return x * y if __name__ == ' ...

  8. Linux设置下载站点

    https://blog.csdn.net/jfhkd2012/article/details/50912757

  9. poj 1759 二分搜索

    题意:N个等差数列,初项X_i,末项Y_i,公差Z_i,求出现奇数次的数? 思路: 因为只有一个数出现的次数为奇数个 假设 第二个数字的个数为 奇数个,其余全部都是偶数个 ,累计出现的次数 a1偶数 ...

  10. 设置默认以管理员运行的WinForm

    右键工程名, 属性; 选择"安全性"; 勾选"启用ClickOnce安全设置"与"这是完全可信的应用程序"; 退出该页面, app.mani ...