海亮 7.20 二分图专题
海亮 7.20 二分图专题
P6185 [NOI Online #1 提高组] 序列
先将所有点的点权赋值为\(a[i]-b[i]\) 最终我们要达到的目标就是
我们先看\(2\)操作 对于\(2\)操作连通的所有点 它们的总和一定是不变的 那么可以用并查集给它们缩点
对于其他\(1\)操作 可以互相连边 再对整张图做一次染色
染色结果分情况讨论:
如果整张图是二分图 那么可以在保证左面点权值总和和右面点权值总和的差不变的情况下随意加减
如果不是二分图 那么可以在保证整体权值奇偶性不变的情况下随意加减
所以非法情况就是:
- 是二分图但左面点和右面点权值不相等
- 不是二分图 整体权值是奇数
有一些需要注意的点:
- 因为我们在染色时还要同时统计左面点和右面点的权值 所以一旦遇到不是二分图的情况 我们也不能直接返回 而是要将整个连通块都扫描完再返回
- 并查集初始化 并查集合并的时候需要判断\(xx!=yy\)
- 因为不保证连通 所以需要染色多次
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define mid (l+r>>1)
#define pb push_back
#define int long long
const int N = 2e5 + 5;
int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int n , m , a[N] , b[N] , op[N] , u[N] , v[N] , col[N] , sum[2];
vector<int> e[N];
int fa[N] , val[N];
int find ( int x ) { return x == fa[x] ? x : fa[x] = find(fa[x]); }
void merge ( int x , int y ) { int xx = find(x) , yy = find(y); if ( xx != yy ) fa[xx] = yy , val[yy] += val[xx]; }
int dfs ( int u , int k )
{
col[u] = k , sum[col[u]] += val[u];
bool pd = 1;
for ( auto v : e[u] )
{
if ( col[u] == col[v] ) pd = 0;//这里不直接return的原因是我们还需要统计所有的sum值
if ( col[v] != -1 ) continue;
if ( !dfs ( v , k ^ 1 ) ) pd = 0;
}
return pd;
}
//目标是让所有东西都为0
signed main ()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
int T = read();
while ( T -- )
{
n = read() , m = read();
for ( int i = 1 ; i <= n ; i ++ ) fa[i] = i , col[i] = -1 , e[i].clear() , val[i] = 0;
for ( int i = 1 ; i <= n ; i ++ ) a[i] = read();
for ( int i = 1 ; i <= n ; i ++ ) b[i] = read() , val[i] = a[i] - b[i];
for ( int i = 1 ; i <= m ; i ++ )
{
op[i] = read() , u[i] = read() , v[i] = read();
if ( op[i] == 2 ) merge ( u[i] , v[i] );
}
for ( int i = 1 ; i <= m ; i ++ ) if ( op[i] == 1 ) e[find(u[i])].pb(find(v[i])) , e[find(v[i])].pb(find(u[i]));
bool pd = 1;
for ( int i = 1 ; i <= n ; i ++ )
if ( find(i) == i && col[i] == -1 )//不保证图连通 所以可能会染色多次
{
sum[0] = sum[1] = 0;
int kk = dfs ( i , 0 );
if ( kk && ( sum[0] != sum[1] ) ) { pd = 0; break; }//一定不能到0
if ( !kk && ( ( sum[0] + sum[1] ) & 1 ) ) { pd = 0; break; }//和为奇数 不能到0
}
cout << ( pd ? "YES" : "NO" ) << endl;
}
return 0;
}
Maximize Mex
考虑将每一个社团和能力值抽象成一个点 社团和能力值互相连边
注意匈牙利连双向边的话我们可以将社团点\(+n\)处理
其实可以不断删边 但是很麻烦
所以考虑将删边转化为加边(时间倒流)然后不难发现\(mex\)是从后到前递增的
那么可以从上一次的位置开始搜索 直到无法找到匹配
需要注意:每一次搜索之前需要\(memset\) \(vis\)数组
正确性:
一定不会出现硬将当前这个能力值匹配上而前面的能力值退让的情况
因为匈牙利算法就是如果当前这个点\(u\)原来的连边的终点\(v\)已经有匹配了
那么让那个终点\(v\)原先匹配的点\(u'\)去找另一个匹配 如果可以找到 那么退让 否则不退让 点\(u\)失配
所以这个\(u\)点可以加入的前提就是前面的东西都能匹配到另一个点 保证了正确性
\(upd:\)\(vis\)数组的作用是禁止\(u'\)再次选择\(v\) 所以每次想进行新一次匹配的时候都要清空
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define mid (l+r>>1)
#define pb push_back
const int N = 1e4 + 5;
int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int n , m , d , p[N] , c[N] , k[N] , mark[N] , link[N] , vis[N] , res , ans[N];
vector<int> e[N];
void add ( int u , int v ) { e[u].pb(v); }
int hungry ( int u )
{
for ( auto v : e[u] )
if ( !vis[v] )
{
vis[v] = 1;
if ( link[v] == -1 || hungry ( link[v] ) ) return link[v] = u , 1;
}
return 0;
}
signed main ()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
memset ( link , -1 , sizeof link );
n = read() , m = read();
for ( int i = 1 ; i <= n ; i ++ ) p[i] = read();
for ( int i = 1 ; i <= n ; i ++ ) c[i] = read();
d = read();
for ( int i = 1 ; i <= d ; i ++ ) k[i] = read() , mark[k[i]] = 1;
for ( int i = 1 ; i <= n ; i ++ ) if ( !mark[i] ) add ( p[i] , c[i] + n ) , add ( c[i] + n , p[i] );
for ( int i = d ; i ; i -- )
{
for ( int j = res ; ; j ++ )
{
memset ( vis , 0 , sizeof vis );
if ( !hungry(j) ) { res = j; break; }
}
ans[i] = res;
add ( p[k[i]] , c[k[i]] + n ) , add ( c[k[i]] + n , p[k[i]] );
}
for ( int i = 1 ; i <= d ; i ++ ) cout << ans[i] << endl;
return 0;
}
#C. 「2017 山东一轮集训 Day2」Pair
前置芝士:霍尔定理
从二分图左边任选一个点集 \(S\),取出与 \(S\) 中的点有边相连的右边的点,构成邻集 \(T\)。若对于任意一个 \(S\) 都满足 \(|S| \le |T|\),则二分图最大匹配存
那么我们在用的时候 需要尝试找到一个\(|S|>|T|\)的情况
那么我们设置\(S\)中权值最大的点为\(a_x\) 显然\(S\)对应的\(T\)是由\(a_x\)唯一确定的(因为所有权值小于\(a_x\)的点的连边 \(a_x\)都可以连上) 如果我们想让\(|S|>|T|\)的情况 那么就要尽可能地让\(S\)大 可以发现最大是将所有\(\le a_x\)的点全部塞进\(S\)
下面讲做法:
我们先将\(b\)数组排序 对于一个\(a_i\) 合法的配对只会出现在\(b\)的一段后缀中
对于一个二分图,我们有 \(O(n)\) 的检验方法:对于每一个 \(i\),统计这个二分图内 \(aj <= ai\) 的 \(j\) 的个数 p,判断是否 \(p - f[i] > 0\),如果是则无法匹配。因为
考虑每次移动区间只会加一个点或者减一个点 那么用线段树维护\(min(f_i-i)\)即可 其中\(f_i\)为被这个\(b[i]\)被边覆盖的数量
初始化值赋为\(-i\)
直接在后缀序列上操作 每次加入一个点就将合法的\(b_j\)位置都\(++\)
合法答案当且仅当\(1-m\)位置上的值都大于等于\(0\)
这样的正确性来自:\(b_i\)的第一个位置只需要\(1\)个匹配即可 因为所有所有\(a_i\)都优先要照顾这个点 往后也是类推的 第二个位置需要两个匹配 因为前面有一个匹配必然需要给到第一个位置
如果不判断\(x>y\)的话 样例只能过一个 但是能\(AC\) 判断后样例都可过()
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define mid (l+r>>1)
#define ls p<<1
#define rs p<<1|1
#define lson ls,l,mid
#define rson rs,mid+1,r
constexpr int N = 150000 + 5;
constexpr int inf = 0x3f3f3f3f;
char buf[1<<22] , *p1 , *p2;
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<22,stdin),p1==p2)?EOF:*p1++)
int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar(); }
return x * f;
}
//int read ()
//{
// int x = 0 , f = 1;
// char ch = cin.get();
// while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
// while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
// return x * f;
//}
int n , m , b[N] , a[N] , h , pos;
struct DQY
{
struct node { int add , val; } t[N<<2];
void up ( int p ) { t[p].val = min ( t[ls].val , t[rs].val ); }
void add ( int p , int val ) { t[p].add += val , t[p].val += val; }
void down ( int p ) { if ( t[p].add ) add ( ls , t[p].add ) , add ( rs , t[p].add ) , t[p].add = 0; }
void build ( int p , int l , int r )
{
if ( l == r ) return t[p].val = -l , void();
build ( lson ) , build ( rson ) , up(p);
}
void upd ( int p , int l , int r , int x , int y , int val )
{
if ( x > y ) return;
if ( x <= l && r <= y ) return add ( p , val ) , void();
down(p);
if ( x <= mid ) upd ( lson , x , y , val );
if ( mid + 1 <= y ) upd ( rson , x , y , val );
up(p);
}
int query ( int p , int l , int r , int x , int y )
{
if ( x <= l && r <= y ) return t[p].val;
down(p);
int res = inf;
if ( x <= mid ) res = min ( res , query ( lson , x , y ) );
if ( mid + 1 <= y ) res = min ( res , query ( rson , x , y ) );
return res;
}
}T;
signed main ()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
n = read() , m = read() , h = read();
for ( int i = 1 ; i <= m ; i ++ ) b[i] = read();
for ( int i = 1 ; i <= n ; i ++ ) a[i] = read();
sort ( b + 1 , b + m + 1 );
T.build ( 1 , 1 , m );
for ( int i = 1 ; i <= m ; i ++ ) pos = lower_bound ( b + 1 , b + m + 1 , h - a[i] ) - b , T.upd ( 1 , 1 , m , pos , m , 1 );
int ans = ( T.t[1].val >= 0 );
for ( int i = m + 1 ; i <= n ; i ++ )
{
pos = lower_bound ( b + 1 , b + m + 1 , h - a[i-m] ) - b , T.upd ( 1 , 1 , m , pos , m , -1 );
pos = lower_bound ( b + 1 , b + m + 1 , h - a[i] ) - b , T.upd ( 1 , 1 , m , pos , m , 1 );
ans += ( T.t[1].val >= 0 );
}
cout << ans << endl;
return 0;
}
Fishermen
水黑()
先将\(a[i]\)的所有倍数离散化 再让它们对应地和原数连边
从小到大地枚举所有倍数 贪心地取最小的情况 如果可以匹配就加这个倍数
我们对于不合法的情况可以不清空\(vis\)数组来优化复杂度
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define mid (l+r>>1)
#define pb push_back
#define int long long
const int N = 1e3 + 5;
char buf[1<<22] , *p1 , *p2;
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<22,stdin),p1==p2)?EOF:*p1++)
int read ()
{
int x = 0 , f = 1;
char ch = getchar();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = getchar(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = getchar(); }
return x * f;
}
int n , a[N] , link[N] , vis[N] , ans;
vector<int> e[N*N] , lsh;
void add ( int u , int v ) { e[u].pb(v); }
int hungry ( int u )
{
for ( auto v : e[u] )
if ( !vis[v] )
{
vis[v] = 1;
if ( link[v] == -1 || hungry ( link[v] ) ) return link[v] = u , 1;
}
return 0;
}
signed main ()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
memset ( link , -1 , sizeof link );
n = read();
for ( int i = 1 ; i <= n ; i ++ )
{
a[i] = read();
for ( int j = 1 ; j <= n ; j ++ ) lsh.pb(a[i]*j);
}
sort ( lsh.begin() , lsh.end() );
int sz = unique ( lsh.begin() , lsh.end() ) - lsh.begin();
lsh.resize(sz);
for ( int i = 1 ; i <= n ; i ++ )
for ( int j = 1 ; j <= n ; j ++ )
add ( lower_bound ( lsh.begin() , lsh.end() , a[i] * j ) - lsh.begin() , i );
for ( int i = 0 ; i < lsh.size() ; i ++ )
if ( hungry(i) ) ans += lsh[i] , memset ( vis , 0 , sizeof vis );
cout << ans << endl;
return 0;
}
Alice and Recoloring 2
观察题面可以发现\(2,3\)操作无用(都可以被两次\(1\))代替
所以我们只需要合理安排\(1,4\)操作即可
考虑对每一个点 处理它本身,右下,右,下四个点值的异或和 记录在\(a\)数组中 那么我们的目标就是将整张图变成全\(0 \)
我们钦定整张图的左上角是固定的 右下角是无限扩展的 也就是允许\(m+1\)和\(n+1\)的存在
一次在\((x,y)\)的\(1\)操作只会将\(x,y\)这个点翻转 因为在边角的点翻转了两次,中间的点翻转了四次 相当于没有更新
一次在\((x,y)\)的\(4\)操作会将\((x,y)\)和\((x,m)\)和\((y,n)\)这些点都翻转 因为这些点所管辖的点只有一个点被翻转了
所以我们用\(4\)操作成功节省了\(1\)的花费
但是我们每一行每一列只能用一次\(4\)操作 因为如果用多了 相当于用两次\(4\)操作修改了\(4\)个点 不比\(1\)操作优
所以 如果\(4\)操作可以翻转三个点 那么横纵坐标连边 跑二分图最大匹配即可 记录匹配的次数
最后需要特判匹配次数的奇偶性和\((n,m)\)的点值不同的情况 此时需要再用一次\(1\)操作来改回来
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define mid (l+r>>1)
#define pb push_back
const int N = 1e3 + 5;
int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int n , m , link[N] , vis[N] , ans , mp[N][N] , a[N][N] , res;
vector<int> e[N*N] , lsh;
void add ( int u , int v ) { e[u].pb(v); }
int hungry ( int u )
{
for ( auto v : e[u] )
if ( !vis[v] )
{
vis[v] = 1;
if ( link[v] == -1 || hungry ( link[v] ) ) return link[v] = u , 1;
}
return 0;
}
char ch;
string s;
signed main ()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
memset ( link , -1 , sizeof link );
n = read() , m = read();
for ( int i = 1 ; i <= n ; i ++ )
for ( int j = 1 ; j <= m ; j ++ )
cin >> ch , mp[i][j] = ( ch == 'B' );
for ( int i = 1 ; i <= n ; i ++ )
for ( int j = 1 ; j <= m ; j ++ )
a[i][j] = mp[i][j] ^ mp[i+1][j] ^ mp[i][j+1] ^ mp[i+1][j+1];
for ( int i = 1 ; i < n ; i ++ )
for ( int j = 1 ; j < m ; j ++ )
if ( a[i][j] && a[i][m] && a[n][j] ) add ( i , j + n ) , add ( j + n , i );
for ( int i = 1 ; i < n ; i ++ ) res += hungry(i) , memset ( vis , 0 , sizeof vis );
ans -= res;
a[n][m] ^= ( res & 1 );
for ( int i = 1 ; i <= n ; i ++ )
for ( int j = 1 ; j <= m ; j ++ )
ans += a[i][j];
cout << ans << endl;
return 0;
}