题面

[传送门](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

拒绝压行,从我做起

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cassert>
#include<stack>
#include<vector>
#define MOD 1000000007
#define ll long long
using namespace std;
inline int read(){
int re=0,flag=1;;char ch=getchar();
while(!isdigit(ch)){
if(ch=='-') flag=-1;
ch=getchar();
}
while(isdigit(ch)) re=(re<<1)+(re<<3)+ch-'0',ch=getchar();
return re*flag;
}
ll inv[200010],fac[200010];
namespace topo{
int first[200010],cnte;
vector<int>e[200010];
int inbound[200010],worked[200010];
vector<int>appearance;
inline void add(int u,int v){
e[u].push_back(v);
inbound[v]++;
}
int siz[200010];
ll dfs(int u,int f){
ll re=1;siz[u]=1;
assert(u);
for(auto v:e[u]){
if(v==f) continue;
(re*=dfs(v,u))%=MOD;
siz[u]+=siz[v];
}
return re*inv[siz[u]]%MOD;
}
ll solve(){
ll re=1,tmp;
for(auto u:appearance){
if(!inbound[u]&&!worked[u]){
tmp=dfs(u,0);
worked[u]=1;
(re*=tmp%MOD)%=MOD;
}
}
for(auto u:appearance){
e[u].clear();
inbound[u]=0;
worked[u]=0;
}
return re;
}
}
vector<int>e[200010];
stack<int>s;
bool vis[200010],on_circle[200010];int cnt1,cnt2,lped;
vector<int>circle;
void dfs(int u,int f){
int i,v;cnt1++;s.push(u);
vis[u]=1;
for(i=0;i<e[u].size();i++){
v=e[u][i];if(v==f||on_circle[v]) continue;
cnt2++;
if(!lped&&vis[v]){
lped=1;
while(s.top()!=v){
circle.push_back(s.top());
on_circle[s.top()]=1;
s.pop();
}
circle.push_back(s.top());
on_circle[s.top()]=1;
s.pop();
}
else if(!vis[v]){
dfs(v,u);
}
}
if(!s.empty()&&s.top()==u) s.pop();
}
int control[200010];
void dfs2(int u,int f){
int i,v;
for(i=0;i<e[u].size();i++){
v=e[u][i];
if(v==f||on_circle[v]) continue;
control[v]=u;
dfs2(v,u);
}
}
void adde(int u,int f){
topo::appearance.push_back(u);
for(auto v:e[u]){
if(v==control[u]) break;
topo::add(u,v);
}
for(auto v:e[u]){
if(v==f||on_circle[v]) continue;
adde(v,u);
}
}
ll solve(int u){
cnt1=cnt2=lped=0;
dfs(u,0);
if(cnt1!=cnt2) return 0;
int i;ll re=0;
for(i=0;i<circle.size();i++){
dfs2(circle[i],0);
}
//first
for(i=0;i<circle.size();i++){
control[circle[i]]=circle[(i+1)%circle.size()];
}
for(i=0;i<circle.size();i++){
adde(circle[i],0);
}
re+=topo::solve();
topo::appearance.clear();
//second
for(i=0;i<circle.size();i++){
control[circle[i]]=circle[(i-1+circle.size())%circle.size()];
}
for(i=0;i<circle.size();i++){
adde(circle[i],0);
}
re+=topo::solve();
topo::appearance.clear();
for(i=0;i<circle.size();i++) on_circle[i]=0;
circle.clear();
return re%MOD;
}
int n;
int main(){
n=read();int i,t1,t2;
memset(topo::first,-1,sizeof(topo::first));
inv[1]=1;fac[1]=1;
for(i=2;i<=n*2;i++) inv[i]=(MOD-MOD/i)*inv[MOD%i]%MOD;
for(i=2;i<=n*2;i++) fac[i]=fac[i-1]*i%MOD;
for(i=1;i<=n*2;i++){
t1=read();t2=read();t2+=n;
e[t1].push_back(t2);
e[t2].push_back(t1);
}
for(i=1;i<=n*2;i++){
if(e[i].size()) sort(e[i].begin(),e[i].end());
}
ll ans=1;
for(i=1;i<=n*2;i++){
if(!vis[i]) (ans=ans*solve(i))%=MOD;
}
cout<<(fac[2*n]*ans)%MOD<<'\n';
}

[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. 使用android ndk编译x86 so在linux下使用的问题

    一直以为android ndk编译x86 so库可以在linxu下运行,结果我试了几次都行不通.后来想了一下,android ndk编译的库应该只能在android设备或模拟器上运行才有效,后来改用 ...

  2. C#基础-委托与事件

    委托 delegate是申明委托的关键字 返回类型都是相同的,并且参数类型个数都相同 委托声明 delegate double DelOperater(double num1, double num2 ...

  3. [Wolfgang Mauerer] 深入linux 内核架构 第一章 概述

    作为Linux开发爱好者,从事linux 开发有两年多时间.做过bsp移植,熟悉u-boot代码执行流程:看过几遍<linux 设备驱动程序开发>,分析过kernel启动流程,写过驱动,分 ...

  4. 【PHP项目】【Smarty】Smarty截取字符串方法truncate

    smarty truncate 截取字符串  //在sql中truncate是删除表格(truncate是只删除内容,delete彻底删除)从字符串开始处截取某长度的字符,默认的长度为80指定第二个参 ...

  5. html5中的progress兼容ie,制作进度条样式

    html5新增的progress标签用处很大,它可以制作进度条,不用像以前那样用css来制作进度条! 一.progress使用方法 progress标签很好使用,他有两个属性,value和max,va ...

  6. jpeglib的使用

    1. 解压jpeglib tar xvzf libjpeg-turbo-1.2.1.tar.gz 2. 阅读里面的说明文件,得到jpeg解压缩的一般步骤: /*Allocate and initial ...

  7. POJ3682 概率DP

    King Arthur's Birthday Celebration Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 3575 ...

  8. git回滚到指定commit

    一次性commit好多文件,push上去之后,发现工程不可用,只能回滚,上网搜索回滚办法,下边这个是自己亲试的,特别好使: 操作步骤: 1.git checkout the_branch 2.git ...

  9. Ubuntu下配置LAMP + PhpStorm

    本文仅作为一个记录,以下配置在Ubuntu 14.10 64-bit上验证通过. 安装Apache 2:sudo apt-get install apache2 安装成功能够后,通过浏览器访问loca ...

  10. 2,Flask 中的 Render Redirect HttpResponse

    一,Flask中的HTTPResponse 在Flask 中的HttpResponse 在我们看来其实就是直接返回字符串 二,.Flask中的Redirect 每当访问"/redi" ...