网络流/二分图相关笔记(干货篇)
command_block · · 算法·理论
备忘 : 随机化一般图最大匹配
请配合 : 网络流/二分图相关笔记(应用篇) 食用
0.前面的话
网络流作为一个玄学算法,其题目难度主要在构图上。
至于网络流本身的算法模板,(如果不精细要求复杂度的话)实现起来难度不大。
在此就不多讲模板的实现了,只简单讲讲我对网络流的理解,如果模板部分看不懂建议先找别的文章学习。
本文内容:
主要是模板。
-
网络流模型的构建
-
性质 : 流量守恒,反对称性,流量范围
-
基本结构 : 增广路,反向边,残量网络
-
与线性规划的关系 (咕咕咕)
-
-
几种常见最大流算法以及复杂度说明
-
Ford-Fulkerson
-
Edmonds–Karp
-
Dinic (包括一些奇怪的优化)
-
-
关于网络流/二分图的常见模型
-
二分图 : 匹配 , 最小点覆盖 , 独立集 , Hall定理
-
最小路径覆盖
-
闭合子图问题
-
偏序集 : 最小连覆盖 , 最长反链
-
-
费用流
-
EK费用流
-
zkw费用流
-
消圈费用流 (咕咕咕)
-
带有负环的费用流
-
费用流与凸性的关系 (咕咕咕)
-
-
上下界网络流
-
上下界无源汇可行流
-
上下界有源汇可行流
-
上下界有源汇最大流
-
上下界有源汇最小流
-
上下界有源汇费用可行流
-
上下界无源汇费用可行流
-
-
最小割树及部分证明
1.网络流引入
我们这一节的目标就是切掉 P3376 【模板】网络最大流 ,并且要尽量跑得快些。
-
网络流问题:
想象一些有向的水管,每个水管都有固定的流量上限,有源点可以出水,有汇点可以收水,问汇点单位时间最多可以收到多少水。
-
一些性质
\large{\color{red}*}
设
1.
2.
注意,如果
即:
(个人认为反对称性是最重要的)
3.
即每个点流入和流出的流量相同,简称流量守恒(当然源点和汇点不满足)
但是显然,源点流出多少,汇点就流入多少,所以
只要满足上述性质,就是个合法的流,读者可以自行验证一下。
也就是说我们成功地把水流问题抽象成了数学模型。
我们的目标就是让
-
增广路,残量网络与反向边
我们设
几次操作之后
容易发现由于
找到残量网络中从
(为什么叫做增广路呢,因为水顺着这条路流过去,就可以增加目前流量啊)
然后把最大流加上这些边的最小容量,再把这些边的容量都减去这个值,相当于更新了一次残量网络。
增广一次不只让正向的边流量增加了,还让反向的边流量减少了。
具体来说,假如增广路中有
所以
这显得比较魔幻:有些边(剩余)的容量反而变大了?
这有一个绝妙的解释,如果一个流流过本次容量变大的边(即反向边),相当于撤销本次流量,即反悔操作。
反复寻找增广路,直到找不到为止,此时得到的就是最大流。
为什么呢?考虑反证法: 假如还有更大的流,那么肯定会有增广路,口胡如下:
我们的流对应的数值是
那么相应地,至少会有一个
相应地
如此传递下去,假如没有传到
-
算法
设
1.Ford-Fulkerson(FF)
非常直觉的算法,直接dfs
寻找增广路,找到就增广,找不到为止。
这个算法的复杂度是多少?会不会一直能找到增广路而陷入死循环呢?
由于最大流明显有限,每次增广当前流量必然增加,实际上是不会陷入死循环的。
但是dfs
被出题人随便骗着玩啊,所以复杂度是
原因就是每次寻找增广路需要
值得注意的是,FF
算法在二分图上跑的时候,表现很类似匈牙利算法。
虽然这个算法价值不大,还是要多说几句 : 如果某道题目中最大流量很小,可以按顺序依次加边,在残量网络上继续增广,会得到
2.Edmonds–Karp(EK)
在FF
的基础上,改用bfs
找增广路(忽略容量为0的边)。
然而,复杂度神奇地变得与值域无关了,变成了
原因 : 使用bfs
找到的必然是当前含边数最少的一条增广路,找一次需要
假如要利用反向边的话,必须走到一条边的尽头再往回走,这样一定比当前的最短路要大,所以是不会产生的,这样子我们就不用考虑容量改变的反向边。
每条增广路都有一个瓶颈,而两次增广的瓶颈不可能相同,所以增广路最多
而增广路的长度是
3.Dinic
这玩意实现起来难度不大,跑起来松松松,实战中应用最广。
首先,建立bfs最短路DAG(成为分层操作),然后只走DAG上的边一次dfs
多路寻找增广路,当找不到的时候再次分层。
如何判断一条边dis[u]+1==dis[v]
同EK,同一个分层图中增广路不超过
多路增广,就是找到了一条增广路之后不马上停止,剩余的容量试试下一条边,经过下面的优化,一次多路增广复杂度是
一次多路增广能够找到当前DAG
中的所有增广路,迫使增广路变长(边数变多)。
那么要分
优化:
-
当前弧优化
记录每个点已经试过了哪些流不出流量的边,下次来就不试了,可以把一次多路增广复杂度严格控制在
O(nm) 内。证明 : 增广次数为
O(m) ,每次增广最多经过O(n) 个点,总复杂度为O(nm) 。不写这个优化,复杂度是错的,可能会退化成
O(nm^2) 。 -
点优化
假如从一个点流不出流量,则把该点的
dis
变为-1 ,这样这一次多路增广再也不会来了。大多数情况下这只能优化常数,但是在某些毒瘤题里面跑的很快。
-
不完全bfs优化
注意到我们bfs到
T 之后就可以停止了,因为后面的路径一定更长。在随机数据,增广路较短(如匹配问题)里能起到不错的效果,且不会增大常数。
某些需要退流(只涉及部分残量网络以提高效率)的题目,采用这个优化会有更加玄学的加成。
-
交替忽略反向边
比较神奇的优化,目前只由@negiizhao巨神卡到
O(n^{1.5}m) 首先忽略反向边跑一次完整的dinic,此时已经得到了一个近似的较优解。
(注意这里如果每次把
0 边删掉,那么一次忽略反向边的dinic复杂度是O(nm) )再考虑反向边重新跑一次多路增广,最后再次分层。
在随机数据里表现极佳,但是代码量略大。
实战中一般用前三个就够了,复杂度是
注意,由于边都是成对加入,如果使用邻接链表,则编号连续。
访问反向边时可以直接 xor 1
,这样要把开始编号设为
如果是 vector
写法,只能大力记下反向边的编号。
Code:
- 加上{多路增广,当前弧优化,点优化,不完全BFS优化}的
dinic
: (最常用标准版)
下面是邻接链表的写法,需要vector
写法请查看评测记录。
#include<cstdio>
#include<queue>
#define Maxn 10500
#define Maxm 200050
#define INF 1000000000
using namespace std;
int n,m,S,T;
struct Line
{int nxt,t,cap;}l[Maxm];
int tl=1,fir[Maxn],dis[Maxn],cur[Maxn];
void addLine(int f,int t,int cap){
l[++tl]=(Line){fir[f],t,cap};fir[f]=tl;
l[++tl]=(Line){fir[t],f,0};fir[t]=tl;
}
bool bfs()
{
static queue<int> q;
for (int i=1;i<=n;i++)
{cur[i]=fir[i];dis[i]=0;}
q.push(S);dis[S]=1;
while(!q.empty()){
int u=q.front();q.pop();
for (int i=fir[u];i;i=l[i].nxt)
if (l[i].cap&&!dis[l[i].t]){
dis[l[i].t]=dis[u]+1;
if (l[i].t==T){
while(!q.empty())q.pop();
return 1;
}q.push(l[i].t);
}
}return 0;
}
int dfs(int u,int flow)
{
if (u==T)return flow;
int sum=0;
for (int &i=cur[u];i;i=l[i].nxt)
if (dis[l[i].t]==dis[u]+1&&l[i].cap){
int sav=dfs(l[i].t,min(flow,l[i].cap));
if (sav){
l[i].cap-=sav;
l[i^1].cap+=sav;
sum+=sav;flow-=sav;
if (!flow)return sum;
}else dis[l[i].t]=-1;
}
return sum;
}
int main()
{
scanf("%d%d%d%d",&n,&m,&S,&T);
for (int i=1,f,t,cap;i<=m;i++){
scanf("%d%d%d",&f,&t,&cap);
addLine(f,t,cap);
}int ans=0;
while(bfs())ans+=dfs(S,INF);
printf("%d",ans);
return 0;
}
评测记录(vector) & 评测记录(邻接链表)
图比较稠密的时候vector
快(耗时可低至邻接链表的50%~75%
),模板题中数据水且点较多,vector
建图常数大体现不出优势。
- 加上{多路增广,当前弧优化,点优化,不完全bfs优化,交替忽略反向边}的dinic:
评测记录 : Loj#127. 最大流 加强版 (差一个点就卡过去了……)
2.网络流相关经典模型
并不一定需要先完全掌握,可以跳过一部分,先去做题。
-
最小割定理
有源汇割 : 一个边集,删去之后使得源汇不连通,而且其中任意一条边不割,则造成源汇连通(割是紧的)
有源汇最小割 : 一个有向图,边有边权(一般为正),要求割去权值和最小的边集使得源汇不连通。
-
①最大流
\leq 最小割首先根据割的定义,所有的流都必然经过割边集中的某一条边,那么流量总和最大就是割边集总和。
-
②最大流
\geq 最小割考虑我们求出了一个最大流,那么某些边会成为瓶颈,即残量网络上为
0 .这些边一定分布成为一个割,否则仍然会有增广路。
最小割的构造 :
跑完最大流之后,从原点选取非BFS
,把这些点成为
一端在
-
例题① : P4662 [BalticOI 2008]黑手党
这题可以割点而不能割边,我们把点拆成出点入点,中间相连的边为点权即可。
评测记录
-
例题② : P4126 [AHOI2009]最小割
最小割的可行边和必须边(所有割集的交集和并集)
题解 P4126 【[AHOI2009]最小割】
图的基本芝士
-
图的匹配 : 选出某些边,使得每两个边没有公共端点。
-
图的独立集 : 选出某些点,使得每两个点没有连边。
-
图的点覆盖 : 选出某些点,每条边都至少有一个端点被选择。
-
闭合子图 : 有向图的子图,满足没有指向子图外的出边。
-
图的路径覆盖 : 在
DAG
上用(边)不相交的简单路径覆盖所有的节点。
二分图相关
-
二分图 : 分为两个部分的图(称为左部和右部),同一个部分内没有边。
-
二分图判定 : 充要条件 : 没有奇环。
dfs
染色判定即可。 -
二分图最大匹配 : 二分图中边最多的匹配(可能有多种方案)。
-
P3386 【模板】二分图匹配
网络流建模方法见第三节。当然也可以用二分图专用算法解决。
另附 : Dinic 算法解决二分图匹配的复杂度上界是
可见 Itst : Dinic 二分图匹配 / Hopcroft-Karp 算法 复杂度简单证明
- 二分图最小点覆盖 :
理解&构造 :
-
①二分图角度
首先,最大匹配中的
a 条边是独立的,所以覆盖这些边都必须要a 个点。我们称有匹配边相连的点为匹配点,反之为空点。
对于一个二分图的最大匹配,原图的任意一条边都不可能两端都是空点,否则可以在匹配中加入这条边。
所以,我们只考虑匹配边端点的一个子集就能够覆盖所有空点。
①对于每条匹配边,我们选且只选一个端点。
②而一端是匹配点一端是空点的边,必选匹配点。
这两项是不会产生矛盾的,如果一条匹配边两端都被②钦定要选,就造成了
0-1=1-0 的增广路,这与最大匹配相悖。构造方法就是先根据②钦定,未被钦定的匹配边就随意选取一端。
-
②网络流最小割角度
我们发现,建立二分图匹配模型之后,该图的最小割正好对应着点覆盖 : 对于一条中间的
\inf 边,左右至少割一个端点。那么通过最小割定理就能自然地引出结论。我们查看那些边被割掉了,也容易构造。(某些优化建边的题目只能这样构造)
例题 : UVA1194 Machine Schedule
- 二分图最大独立集
理解&构造 : 先求出点覆盖集合,取补集则得到最大独立集。
独立 : 如果补集之间有连边,则与点覆盖矛盾。
最大 : 如果这个集合更大,则必然要拆掉点覆盖的一部分,这和“最小”点覆盖矛盾。
总而言之,最大独立集和最小点覆盖是对偶问题。这对一般图也是成立的。
- Hall定理
二分图完美匹配 : 若两侧点集为
我们称
必要性 : 若不满足右侧,则也不满足左侧。
对于某
充分性 : 若不满足左侧,假设其满足右侧。
先构造一种最大匹配,此时有未匹配的点存在。
该未匹配点必然有出边,否则违反右侧。如果出边到达的点也不在匹配中,则可以多匹配一条边,矛盾。
若对面的点存在于匹配中,我们找出与其匹配的点,此时我们拥有两个
根据右侧,
若已经被匹配,就会在
如此可以不断增大集合,然而
- 推论 : 二分图最大匹配为
|X|-\max\big(|S|-|N(S)|\big)
不会用Hall
定理来推……
可以把式子变为
如果建立二分图匹配的网络流模型,把
根据最小割定理得证。(似乎也可以得到经典的Hall
定理?)
- 扩展 : 考虑多重匹配(带容量匹配)的情况,某个点能匹配的次数是
W_u 。
这可以视作将
我们选择
不难发现,由于边是相同的,
这样,若
选择原图中左侧的点集
那么在新图中,
这就引出 : 多重匹配有完美匹配的充要条件 : 对于任意的
闭合子图问题
- 最大权闭合子图 : 点有点权(可正可负),选出点权和最大的闭合子图。
考虑最小割建模。
对于正价点,连源,边权为点权。对应地,负价点连汇,边权为(负)点权。
原图中的有向边保留,边权置为INF
,此时有:
理解 : 根据结论式子,最小割中割去的边,负权点表示选择,正权点表示不选。
那么我们试图证明 : 在一个割中,割去的负权点和未割的正权点组成的子图
如果
如果指向未割过的负权点,则INF
边和该点流向汇,与割矛盾。
如果指向已割过的正权点,则
类似地,如果
构造 : 要求给出闭合子图的方案。
先求出最小割,在闭合子图中的点为 : 未割过的正权点 & 已经割过的负权点。
最小路径覆盖
P2764 最小路径覆盖问题 求所含路径数最少的路径覆盖(点不相交)。
我们采用调整的思想。首先将每个点用单独一个路径覆盖,这是合法的。
每合并两条路径,我们的最终路径数就减小
我们拆出入点变成二分图,对于图中原有的边,从出点连向入点即可。
某条边被选中,就相当于“串起”了两条已有的路径。(按照最大匹配合并路径即可得知最小路径覆盖)
注意到“单条路径”的限制 : 不能分叉。这跟匹配的“左端点不重复”限制是等价的。
不能多条汇聚,这跟匹配的“右端点不重复”限制是等价的。
所以我们就把最小路径覆盖转化成了二分最大匹配。
评测记录
偏序集的最小链覆盖问题
定义 :
-
偏序集 : 元素之间存在大小关系,允许不可比(无定义)的情况出现。但是比较一定有传递性,且不能互相矛盾。
-
链 : 一个子图,任意两个点都可比。(可以跳过一些中间点,不是图论中的链)
-
反链 : 一个子图,任意两个点都不可比。
能够发现,如果把DAG
的有向边看做 >
,那么一定不会互相矛盾。一般来讲,偏序集通常是以DAG
的形式给出的。
现在,需要用尽量少且不重的链,覆盖图上的每一个点。
我们先
不难发现,新图中的一条连续路径,就对应着原图的一条链。我们求出最小路径覆盖即可。
最长反链问题
P4298 [CTSC2008]祭祀 给出一个部分偏序集,求最长的反链。
可以自行了解 Dilworth 定理。这里只给出下界的构造解。
我们考虑 : 最小链覆盖
这个二分图是由出入点和一系列中间的边组成。
注意到有
求出一个最大独立集
先证明
现在证明
把独立集中的点设为黑点,反之是白点。设原图中的点数为
考虑证明这个反链的大小不低于
独立集大小是
后者不会超过
-
附更为简洁的判定方法 :
u 的入点在S 集内,且出点不在\Leftrightarrow u 在最长反链中。证明 : 前文提到,充要条件为 : 出入点均在最大独立集内。
注意到最小点覆盖是最大独立集的补集,这条件相当于 : 出入点均不在最小点覆盖内。
回忆 : 最小点覆盖一定是某条匹配边有闲置边相连的那个端点。
考虑入点,由于其与
S 连通,说明没有流经过入边,也不会造成匹配,自然入点就不是匹配边端点。考虑出点,一种情况是与世隔绝,自然也不是最小点覆盖。另一种情况是与某条匹配边相邻,但是一定没有额外的闲置边,否则将会能从
S 到达。
回到题目,若需要判定某个点能否存在于最长反链内,直接钦定这个点,删除与其冲突的点,再跑一次即可。
评测记录
3.费用流
P3381 【模板】最小费用最大流
在送水模型的前提下,每条边都增加了一个费用,每一单位的流量通过都需要付出相应的费用。
现在要在流量最大的情况下使得总费用最小。
- EK
不难想到,每次找费用最小的增广路进行增广即可。
可以把 EK
算法中的 BFS
改为 SPFA
,直接跑就好了。
复杂度不太对劲,可以卡到指数级,但是正常的出题人一般不会去卡。
#include<cstdio>
#include<vector>
#include<queue>
#define Maxn 5100
#define Maxm 100500
#define INF 2147483647
using namespace std;
int n,m,S,T;
struct Line
{int nxt,t,cap,l;}l[Maxm];
int fir[Maxn],tot=1;
inline void addLine(int f,int t,int cap,int cost)
{
l[++tot]=(Line){fir[f],t,cap,cost};
fir[f]=tot;
l[++tot]=(Line){fir[t],f,0,-cost};
fir[t]=tot;
}
int pre[Maxn],lp[Maxn];
int dis[Maxn],flow[Maxn];
bool in[Maxn];
queue<int> t;
int spfa()
{
for (int i=0;i<=n;i++)
{dis[i]=INF;flow[i]=0;}
flow[S]=INF;dis[S]=0;
t.push(S);in[S]=1;
while(!t.empty()){
int u=t.front();
in[u]=0;t.pop();
for (int i=fir[u],v;i;i=l[i].nxt)
if (l[i].cap&&dis[v=l[i].t]>dis[u]+l[i].l){
dis[v]=dis[u]+l[i].l;
flow[v]=min(flow[u],l[i].cap);
lp[v]=i;pre[v]=u;
if (!in[v]){in[v]=1;t.push(v);}
}
}return flow[T];
}
int main()
{
scanf("%d%d%d%d",&n,&m,&S,&T);
for (int i=1,f,t,cap,cost;i<=m;i++){
scanf("%d%d%d%d",&f,&t,&cap,&cost);
addLine(f,t,cap,cost);
}int d,ans=0,cans=0;
while(d=spfa()){
int u=T;
while(u!=S){
int last=pre[u];
l[lp[u]].cap-=d;
l[lp[u]^1].cap+=d;
u=last;
}ans+=d;cans+=d*dis[T];
}printf("%d %d",ans,cans);
return 0;
}
评测记录(邻接链表)
评测记录(vector)
(由于未知的原因,在洛谷用vector
会比邻接链表快很多)
- zkw
(起因:P3705
中差点TLE
,观察最优解靠前发现写的都是zkw
)
我们发现前面的EK
本质上是沿着最短路增广。
仔细思考最短路的定义,设
则最短路就是需要求解满足如下两个条件的标号。
-
①
D[u]+l(u,v)\geq D[v] -
② 对于某个
v ,至少有一个u 满足D[u]+l(u,v)=D[v]
我们增广之后,某些边的容量变为
EK
的解决方法是 : 暴力跑一次SPFA
。这样没有利用到之前网络的信息,十分浪费。
考虑如下的策略 : 先找到目前能到达的集合,称为
查看目前
能够发现我们至少使得有一条边满足了②,
注意到
可见,若将新的
对于原有的边,则有
同样也满足条件。
不难发现,边修正后的边权总是在减小。
证明 : 割断
-
定理2 任取三点
a,b,c 则Cut(a,b),Cut(a,c),Cut(b,c) 中最小值至少出现两次。证明 : 不妨假设
Cut(a,b) 是最小的,先割开,再讨论c 在S,T 的那个集合内。不妨设其在
S 内,由 {定理1} 得到Cut(b,c)\leq Cut(a,b) 。而由
Cut(a,b) 最小的假设则得到Cut(b,c)=Cut(a,b) ,出现两次。-
推论1 :
Cut(a,b)\geq \min(Cut(a,c),Cut(b,c)) 若
Cut(a,b) 不是最小值,显然成立,若Cut(a,b) 是最小值,则一定会出现两次,也会出现在\min 中。 -
推论2 对于两点
(u,v) ,有Cut(u,v)\geq\min\Big(Cut(u,w_1),Cut(w_1,w_2)...,Cut(w_m,v)\Big) 多次使用 {推论1} 即可。
-
-
定理③(最小割树定理) 如上所述。设
u,v 为欲求的两个点,x,y 为路径上的某两个点。首先,不难得到
u,v 在x,y 最小割的两侧。由 {定理1} ,Cut(u,v)\leq Cut(x,y) 。然后根据 {定理2.2} 又能得到
Cut(u,v)\geq Cut(x,y) 。这就得到了
Cut(u,v)=Cut(x,y) ,鼓掌!
回到模板题,求出最小割树之后,对于每个询问就是树链
#include<cstdio>
#include<vector>
#include<queue>
#define pb push_back
#define INF 1000000000
#define Maxn 505
#define Maxm 1505
using namespace std;
struct Line{int t,c,b;};
vector<Line> wg[Maxn];
#define g wg
void adl(int f,int t,int cap){
g[f].pb((Line){t,cap,g[t].size()});
g[t].pb((Line){f,cap,g[f].size()-1});
}
int n,m,S,T,dis[Maxn],cur[Maxn];
queue<int> q;
bool bfs()
{
for (int i=1;i<=n;i++){cur[i]=dis[i]=0;}
q.push(S);dis[S]=1;
while(!q.empty()){
int u=q.front();q.pop();
for (int i=0,v;i<g[u].size();i++)
if (g[u][i].c&&!dis[v=g[u][i].t]){
dis[v]=dis[u]+1;
if (v==T){
while(!q.empty())q.pop();
return 1;
}q.push(v);
}
}return 0;
}
int dfs(int u,int flow)
{
if (u==T)return flow;
int sum=0;
for (int &i=cur[u],v;i<g[u].size();i++)
if (dis[v=g[u][i].t]==dis[u]+1&&g[u][i].c){
int sav=dfs(v,min(flow,g[u][i].c));
if (sav){
g[u][i].c-=sav;
g[v][g[u][i].b].c+=sav;
sum+=sav;flow-=sav;
if (!flow)break;
}else dis[v]=-1;
}
return sum;
}
void clear()
{for (int i=1;i<=n;i++)g[i].clear();}
#undef g
struct Data
{int f,t,c;}s[Maxm];
int dinic()
{
clear();
for (int i=1;i<=m;i++)
adl(s[i].f,s[i].t,s[i].c);
int ans=0;
while(bfs())ans+=dfs(S,INF);
return ans;
}
vector<int> g[Maxn],l[Maxn];
void adl2(int f,int t,int len){
g[f].pb(t);l[f].pb(len);
g[t].pb(f);l[t].pb(len);
}
int p[Maxn],sp[Maxn];
void solve(int l,int r)
{
if (l==r)return ;
S=p[l];T=p[r];
int len=dinic();
adl2(S,T,len);
int tn=0,mid=l-1;
for (int i=l;i<=r;i++)
if (dis[p[i]])sp[++tn]=p[i];
else p[++mid]=p[i];
for (int i=1;i<=tn;i++)
p[mid+i]=sp[i];
solve(l,mid);solve(mid+1,r);
}
int qt,cut[Maxn][Maxn],*d;
bool vis[Maxn];
void dfs2(int u,int len)
{
vis[u]=1;d[u]=len;
for (int i=0;i<g[u].size();i++)
if (!vis[g[u][i]])
dfs2(g[u][i],min(len,l[u][i]));
}
int main()
{
scanf("%d%d",&n,&m);n++;
for (int i=1;i<=m;i++){
scanf("%d%d%d",&s[i].f,&s[i].t,&s[i].c);
s[i].f++;s[i].t++;
}for (int i=1;i<=n;i++)p[i]=i;
solve(1,n);
for (int u=1;u<=n;u++){
for (int i=1;i<=n;i++)vis[i]=0;
d=cut[u];dfs2(u,INF);
}scanf("%d",&qt);
for (int i=1,f,t;i<=qt;i++){
scanf("%d%d",&f,&t);
printf("%d\n",cut[f+1][t+1]);
}return 0;
}