2023冲刺国赛模拟 17.1
T1 掌分治
由于当前分治中心的贡献为其所在连通块的大小,因此考虑统计每个点 \(u\) ,对于当前分治中心 \(root\) 的贡献,也就是统计当前分治中心 \(root\) 与点 \(u\) 连通的方案数。
设随机选择分治中心形成的序列为 \(p\) ,如果原图是一棵树,不难发现这等价于 \(root\to u\) 路径上所有点在 \(p\) 中出现的位置大于 \(root\) 出现的位置,这很容易进行统计,考虑原图是一棵仙人掌,比较显然的想法是建立圆方树,圆方树上方点连接的所有圆点在仙人掌上构成一个简单环,由于 \(root\to u\) 的路径上的所有方点代表两种路径,因此只需要钦定其中一条连通即可,两条道路同时连通的情况会被重复计算,需要减去,可以使用背包 dp 计算 \(root\to u\) 的路径上,钦定 \(i\) 个点使得 \(root\) 和 \(u\) 连通的容斥系数之和,可以在 \(O(n^3)\) 的复杂度下求解答案。
code
#include <cstdio>
#include <algorithm>
#include <vector>
#include <cstring>
using namespace std;
const int max1 = 400;
const int mod = 998244353;
int n, m;
vector <int> edge[max1 + 5];
vector <int> new_edge[max1 * 2 + 5];
int matrix;
int dfn[max1 + 5], low[max1 + 5], dfs_clock;
int s[max1 + 5], top;
int inv[max1 + 5], fac[max1 + 5], ifac[max1 + 5];
int val[max1 + 5];
int f[max1 + 5][max1 + 5], ans;
int A ( int n, int m )
{
if ( n < m || n < 0 || m < 0 )
return 0;
return 1LL * fac[n] * ifac[n - m] % mod;
}
void Tarjan ( int now )
{
dfn[now] = low[now] = ++dfs_clock;
s[++top] = now;
for ( auto v : edge[now] )
{
if ( !dfn[v] )
{
Tarjan(v);
low[now] = min(low[now], low[v]);
if ( low[v] == dfn[now] )
{
++matrix; int x;
do
{
x = s[top--];
new_edge[matrix].push_back(x);
} while ( x != v );
new_edge[now].push_back(matrix);
}
}
else
low[now] = min(low[now], dfn[v]);
}
return;
}
void Dfs ( int now, int fa )
{
if ( now <= n )
{
for ( int i = 0; i <= n - 1; i ++ )
{
if ( !f[now][i] )
continue;
ans = ( ans + 1LL * f[now][i] * val[i] ) % mod;
}
for ( auto v : new_edge[now] )
Dfs(v, now);
}
else
{
int len = new_edge[now].size() - 1;
for ( int i = 0; i <= len; i ++ )
{
int v = new_edge[now][i];
memset(f[v], 0, sizeof(int) * n);
int x = i + 1;
for ( int k = 0; k <= n - x - 1; k ++ )
f[v][k + x] = ( f[v][k + x] + f[fa][k] ) % mod;
x = len - i + 1;
for ( int k = 0; k <= n - x + 1; k ++ )
f[v][k + x] = ( f[v][k + x] + f[fa][k] ) % mod;
x = len + 1;
for ( int k = 0; k <= n - x + 1; k ++ )
f[v][k + x] = ( f[v][k + x] - f[fa][k] + mod ) % mod;
Dfs(v, now);
}
}
return;
}
int main ()
{
freopen("cactus.in", "r", stdin);
freopen("cactus.out", "w", stdout);
scanf("%d%d", &n, &m);
for ( int i = 1, u, v; i <= m; i ++ )
{
scanf("%d%d", &u, &v);
edge[u].push_back(v);
edge[v].push_back(u);
}
inv[1] = 1;
for ( int i = 2; i <= n; i ++ )
inv[i] = 1LL * ( mod - mod / i ) * inv[mod % i] % mod;
fac[0] = ifac[0] = 1;
for ( int i = 1; i <= n; i ++ )
{
fac[i] = 1LL * fac[i - 1] * i % mod;
ifac[i] = 1LL * ifac[i - 1] * inv[i] % mod;
}
for ( int i = 0; i <= n - 1; i ++ )
for ( int p = 1; p <= n; p ++ )
val[i] = ( val[i] + 1LL * A(n - p, i) * fac[n - i - 1] ) % mod;
for ( int i = 1; i <= n; i ++ )
{
memset(dfn + 1, 0, sizeof(int) * n); dfs_clock = top = 0;
for ( int k = 1; k <= n + n; k ++ )
new_edge[k].clear();
matrix = n;
Tarjan(i);
memset(f[i], 0, sizeof(int) * n);
f[i][0] = 1;
Dfs(i, 0);
}
ans = 1LL * ans * ifac[n] % mod;
printf("%d\n", ans);
return 0;
}
T2 图圈圈
一群人写随机化,实在是太邪恶了。
首先对原图建立 dfs 树,如果存在 \(n\) 条返祖边,显然一定存在两条本质不同,长度相同的简单环。
对于 \(m\le 2n\) 的情况,容易发现我们只需要找到原图中 \(O(n)\) 条本质不同的环就可以判断,那么考虑枚举环上断点 \(root\) 进行 Dfs ,设当前 Dfs 到达的点为 \(u\) ,考虑一条没有被经过的出边 \((u,v)\) ,如果 \(v=root\) ,直接统计当前环,如果 \(v\) 没有被访问过,并且删去原先选择的边和 \((u,v)\) 后, \(root\) 与 \(v\) 连通,此时一定存在环包含原先选择的边和 \((u,v)\) ,因此继续 Dfs 。
容易发现上述过程可以保证每次 Dfs 都是有效的一步(一定能够找到一个环),简单计算复杂度,考虑到环的数量为 \(n\) ,由于环的大小为 \(n\) ,因此一个环会产生 \(n\) 个断点,也就是一个环贡献 \(n\) 次 Dfs ,同时每次 Dfs 需要进行复杂度为 \(O(n)\) 的 Check ,因此总复杂度为 \(O(n^3)\) 。
发现 \(m\) 的上界仍然很松,考虑继续优化。
假设原图不存在两个本质不同,长度相同的环,考虑随机删去原图中 \(\sqrt{n}\) 条边,那么一个长度为 \(L\) 的环存在的概率为 \((1-\tfrac{1}{\sqrt{n}})^L\) ,那么最终环的个数的期望最大为 \(\sum_{i=3}^{n}(1-\tfrac{1}{\sqrt{n}})^L\le \sqrt{n}\) 。然而当 \(m>n+2\sqrt{n}\) 时,任意删去 \(\sqrt{n}\) 条边后对原图建立 Dfs 树一定存在 \(\sqrt{n}\) 条返祖边,因此最终环的个数的期望一定大于 \(\sqrt{n}\) 。
这样可以得到一个较紧的界 \(m\le n+2\sqrt{n}\) ,由于环至少包含一条返祖边,因此可以将所有返祖边涉及到的点单独取出后建立虚树,容易发现虚树大小不超过 \(O(\sqrt{n})\) ,因此压缩后的环长度不超过 \(O(\sqrt{n})\) ,同时 Check 的复杂度不超过 \(O(\sqrt{n})\) ,因此总复杂度为 \(O(n^2)\) 。
code
#include <cstdio>
#include <algorithm>
#include <vector>
#include <cmath>
#include <utility>
#include <iostream>
using namespace std;
const int max1 = 2e4;
int n, m;
vector <int> edge[max1 + 5], tree[max1 + 5];
vector < pair <int, int> > new_edge[max1 + 5];
vector <int> id[max1 + 5];
bool choose[max1 + 5]; int total;
int belong[max1 + 5], cnt, deep[max1 + 5];
int siz[max1 + 5], father[max1 + 5], son[max1 + 5];
int top[max1 + 5], dfn[max1 + 5], rk[max1 + 5], dfs_clock;
int point[max1 + 5], len, s[max1 + 5], stop;
int bin[max1 + 5];
bool vis[max1 + 5], able[max1 + 5];
void YES ()
{
printf("Yes\n");
exit(0);
}
void Dfs ( int now, int fa, int id )
{
belong[now] = id;
deep[now] = deep[fa] + 1;
for ( auto v : edge[now] )
{
if ( v == fa )
continue;
if ( !deep[v] )
{
Dfs(v, now, id);
tree[now].push_back(v);
}
else
{
if ( deep[v] < deep[now] )
{
point[++len] = v;
point[++len] = now;
}
}
}
return;
}
void Find_Heavy_Edge ( int now, int fa, int depth )
{
siz[now] = 1, deep[now] = depth, father[now] = fa, son[now] = 0;
int max_siz = 0;
for ( auto v : tree[now] )
{
Find_Heavy_Edge(v, now, depth + 1);
if ( siz[v] > max_siz )
max_siz = siz[v], son[now] = v;
siz[now] += siz[v];
}
return;
}
void Connect_Heavy_Edge ( int now, int ancestor )
{
top[now] = ancestor;
dfn[now] = ++dfs_clock;
rk[dfs_clock] = now;
if ( son[now] )
Connect_Heavy_Edge(son[now], ancestor);
for ( auto v : tree[now] )
{
if ( v == son[now] )
continue;
Connect_Heavy_Edge(v, v);
}
return;
}
int Get_Lca ( int u, int v )
{
while ( top[u] != top[v] )
{
if ( deep[top[u]] < deep[top[v]] )
swap(u, v);
u = father[top[u]];
}
if ( deep[u] > deep[v] )
swap(u, v);
return u;
}
void Check ( int now )
{
able[now] = true;
int num = 0;
for ( auto v : new_edge[now] )
{
if ( !choose[id[now][num]] && !vis[v.first] && !able[v.first] )
Check(v.first);
++num;
}
return;
}
bool Check ( int root, int target )
{
vis[root] = vis[target] = false;
for ( int i = 1; i <= len; i ++ )
able[point[i]] = false;
Check(root);
vis[root] = true; vis[target] = false;
return able[target];
}
void Solve ( int now, int root, int dis )
{
if ( now != root && !Check(root, now) )
return;
vis[now] = true;
int num = 0;
for ( auto p : new_edge[now] )
{
int v = p.first, w = p.second;
if ( !choose[id[now][num]] )
{
if ( !vis[v] )
{
choose[id[now][num]] = true;
Solve(v, root, dis + w);
choose[id[now][num]] = false;
}
else
{
if ( v == root )
{
bin[dis + w] += w;
if ( bin[dis + w] > dis + dis + w + w )
YES();
}
}
}
++num;
}
vis[now] = false;
return;
}
int main ()
{
freopen("graph.in", "r", stdin);
freopen("graph.out", "w", stdout);
scanf("%d%d", &n, &m);
for ( int i = 1, u, v; i <= m; i ++ )
{
scanf("%d%d", &u, &v);
edge[u].push_back(v);
edge[v].push_back(u);
}
if ( m > n + sqrt(n) + sqrt(n) )
YES();
for ( int i = 1; i <= n; i ++ )
{
if ( !belong[i] )
{
total = len = dfs_clock = 0; ++cnt; Dfs(i, 0, cnt); point[++len] = i;
Find_Heavy_Edge(i, 0, 0); Connect_Heavy_Edge(i, i);
sort(point + 1, point + 1 + len, [&]( const int &x, const int &y ) { return dfn[x] < dfn[y]; });
len = unique(point + 1, point + 1 + len) - ( point + 1 );
int num = len;
for ( int k = 1; k <= num - 1; k ++ )
point[++len] = Get_Lca(point[k], point[k + 1]);
sort(point + 1, point + 1 + len, [&]( const int &x, const int &y ) { return dfn[x] < dfn[y]; });
len = unique(point + 1, point + 1 + len) - ( point + 1 );
stop = 0;
for ( int k = 1; k <= len; k ++ )
{
while ( stop && Get_Lca(point[s[stop]], point[k]) != point[s[stop]] )
--stop;
if ( stop )
{
int dis = deep[point[k]] - deep[point[s[stop]]];
new_edge[point[s[stop]]].push_back(make_pair(point[k], dis));
new_edge[point[k]].push_back(make_pair(point[s[stop]], dis));
id[point[s[stop]]].push_back(++total);
id[point[k]].push_back(total);
choose[total] = false;
}
s[++stop] = k;
}
for ( int k = 1; k <= len; k ++ )
{
int now = point[k];
for ( auto v : edge[now] )
{
if ( deep[v] < deep[now] - 1 )
{
new_edge[v].push_back(make_pair(now, 1));
new_edge[now].push_back(make_pair(v, 1));
id[v].push_back(++total);
id[now].push_back(total);
choose[total] = false;
}
}
}
for ( int k = 1; k <= len; k ++ )
{
for ( int w = 1; w <= len; w ++ )
vis[point[w]] = false;
Solve(point[k], point[k], 0);
}
}
}
printf("No\n");
return 0;
}
T3 下大雨
原先考过,然而没有改。
由于雨滴一定向下落,因此这 \(n\) 个雨棚实际上存在一定的拓扑关系,可以使用 set + 扫描线找到这个拓扑关系。
考虑 dp ,设 \(f_i\) 表示雨滴下落到 \(i\) 时最少需要打几个洞,设当前考虑的雨棚为 \(x\) ,如果当前雨棚左低右高,那么转移是首先对当前雨棚涉及到的区间做后缀 \(\min\) ,然后进行区间 \(+1\) 的操作,如果当前雨棚左高右低,就是区间做前缀 \(\min\) ,然后进行区间 \(+1\) 。
考虑使用 map 维护差分实现上述操作,维护两个 map ,一个维护正数,另一个维护负数,区间 \(+1\) 直接简单插入即可,考虑区间前缀 \(\min\) ,这相当于将当前区间内所有正数差分全部推平,那么在正数差分的 map 中二分,不断删去正数差分,考虑到之后的 dp 值尚未改变,因此删去差分时需要简单修改后面第一个的差分,后缀 \(\min\) 同理。
势能分析发现复杂度为 \(O(n\log n)\) 。
code
#include <cstdio>
#include <algorithm>
#include <vector>
#include <queue>
#include <set>
#include <map>
using namespace std;
const int max1 = 5e5;
const int inf = 0x3f3f3f3f;
int n, L, R;
struct Segment
{
int x1, x2, y1, y2;
}seg[max1 + 5];
struct Set_Segment
{
int id;
Set_Segment () {}
Set_Segment ( int __id )
{ id = __id; }
bool operator < ( const Set_Segment &A ) const
{
int x = max(seg[id].x1, seg[A.id].x1);
return (__int128_t) ( ( 1LL * seg[id].x1 * seg[id].y2 - 1LL * seg[id].x2 * seg[id].y1 ) + 1LL * ( seg[id].y1 - seg[id].y2 ) * x ) * ( seg[A.id].x1 - seg[A.id].x2 ) <
(__int128_t) ( ( 1LL * seg[A.id].x1 * seg[A.id].y2 - 1LL * seg[A.id].x2 * seg[A.id].y1 ) + 1LL * ( seg[A.id].y1 - seg[A.id].y2 ) * x ) * ( seg[id].x1 - seg[id].x2 );
}
};
struct Point
{
int pos, v;
bool operator < ( const Point &A ) const
{
if ( pos == A.pos )
return v > A.v;
return pos < A.pos;
}
};
Point p[max1 * 2 + 5]; set <Set_Segment> Set;
vector <int> edge[max1 + 5]; int deg[max1 + 5];
queue <int> que;
map <int, int> f, g, ans;
void Insert ( int pos, int v )
{
if ( f.find(pos) != f.end() )
v += f[pos], f.erase(pos);
if ( g.find(pos) != g.end() )
v += g[pos], g.erase(pos);
if ( v > 0 )
f.insert(make_pair(pos, v));
if ( v < 0 )
g.insert(make_pair(pos, v));
return;
}
int main ()
{
freopen("rain.in", "r", stdin);
freopen("rain.out", "w", stdout);
scanf("%d%d%d", &L, &R, &n); L <<= 1, R <<= 1;
for ( int i = 1; i <= n; i ++ )
{
scanf("%d%d%d%d", &seg[i].x1, &seg[i].y1, &seg[i].x2, &seg[i].y2);
seg[i].x1 <<= 1, seg[i].x2 <<= 1, seg[i].y1 <<= 1, seg[i].y2 <<= 1;
if ( seg[i].x1 > seg[i].x2 )
swap(seg[i].x1, seg[i].x2), swap(seg[i].y1, seg[i].y2);
p[i].pos = seg[i].x1, p[i].v = i;
p[n + i].pos = seg[i].x2, p[n + i].v = -i;
}
sort(p + 1, p + 1 + n + n);
for ( int i = 1; i <= n + n; i ++ )
{
int v = p[i].v;
if ( v > 0 )
{
if ( !Set.empty() )
{
if ( *Set.begin() < Set_Segment(v) )
{
int tmp = (*--Set.lower_bound(Set_Segment(v))).id;
edge[v].push_back(tmp);
++deg[tmp];
}
if ( Set_Segment(v) < *--Set.end() )
{
int tmp = (*Set.upper_bound(Set_Segment(v))).id;
edge[tmp].push_back(v);
++deg[v];
}
}
Set.insert(Set_Segment(v));
}
else
{
v = -v;
Set.erase(Set_Segment(v));
}
}
Insert(-2e9 - 5, inf);
Insert(L, -inf);
Insert(R + 1, inf);
Insert(2e9 + 5, -inf);
f[-2e9 - 5], g[-2e9 - 5], f[2e9 + 5], g[2e9 + 5];
for ( int i = 1; i <= n; i ++ )
if ( !deg[i] )
que.push(i);
while ( !que.empty() )
{
int x = que.front(); que.pop();
for ( auto v : edge[x] )
{
--deg[v];
if ( !deg[v] )
que.push(v);
}
if ( seg[x].y1 < seg[x].y2 )
{
while ( true )
{
pair <int, int> pos = *--g.upper_bound(seg[x].x2);
if ( pos.first < seg[x].x1 + 1 )
break;
g.erase(pos.first);
int pre = max(seg[x].x1, max((--f.lower_bound(pos.first)) -> first, (--g.lower_bound(pos.first)) -> first));
Insert(pre, pos.second);
}
Insert(seg[x].x1 + 1, 1); Insert(seg[x].x2 + 1, -1);
}
else
{
while ( true )
{
pair <int, int> pos = *f.lower_bound(seg[x].x1 + 1);
if ( pos.first > seg[x].x2 )
break;
f.erase(pos.first);
int nxt = min(seg[x].x2 + 1, min(f.upper_bound(pos.first) -> first, g.upper_bound(pos.first) -> first));
Insert(nxt, pos.second);
}
Insert(seg[x].x1, 1), Insert(seg[x].x2, -1);
}
}
for ( auto v : f )
ans[v.first] += v.second;
for ( auto v : g )
ans[v.first] += v.second;
ans[L], ans[R];
int sum = 0, Min = inf;
for ( auto v : ans )
{
sum += v.second;
if ( v.first >= L && v.first <= R )
Min = min(Min, sum);
}
printf("%d\n", Min);
return 0;
}

浙公网安备 33010602011771号