G - Game on Tree 2

https://atcoder.jp/contests/abc218/tasks/abc218_g

n点, 树, 点i上有数字Ai

初始 棋子在点1, 交替玩, 每次移动到未访问过的相邻点, 直到无法移动为止

先手 希望最大化访问过的中位数, 后手希望最小化中位数

如果他们都最优方案, 求这个中位数

限制

N 1e5

Ai [2,1e9]

我的思路

既然是树, 相当于1作为根, 走到叶节点结束, 路径上的中位数就是结果

换句话说, 每个叶节点 可以存储结果

如果能够算出每个从根到叶的结果, 那么简单的根据深度树上dp就完了(根 同的2倍深度 选最大, 根%2不同深度选最小)

想到的是相当于从中间剖开,那么dfs维护一个大根堆,一个小根堆,让它们元素个数差最多是1即可

代码

https://atcoder.jp/contests/abc218/submissions/33755229

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
#define rep(i,a,n) for (ll i=a;i<(ll)n;i++)
#define pb push_back

ll read(){ll r;scanf("%lld",&r);return r;} // read

int A[100010];
vector<int> e[100010];

template<typename T> using maxSet = multiset<T, greater<T>>;
template<typename T> using minSet = multiset<T>;

int mid[100010];

template<typename T>
void balance(maxSet<T> &small,minSet<T> &large){
while(large.size() > small.size()){
small.insert(*large.begin());
large.erase(large.begin());
}
while(small.size() >= large.size() + 2){
large.insert(*small.begin());
small.erase(small.begin());
}
}

template<typename T>
int dfs(int u,int f,maxSet<T> &small,minSet<T> &large, int dep){
// 插入
if(small.size() == 0 || *small.begin() >= A[u]){
small.insert(A[u]);
}else{
large.insert(A[u]);
}
balance(small,large);

int res = 0;
if(u != 1 && e[u].size() == 1){ // leaf
res = (small.size() == large.size()) ? (*small.begin() + *large.begin())/2 : *small.begin();
}else{
vector<int> vals;
for(auto v:e[u]) if(v != f) vals.push_back(dfs(v,u,small,large,dep^1));
res = (dep==0)? *max_element(vals.begin(),vals.end()) : *min_element(vals.begin(),vals.end());
}
// 删除
auto sptr = small.find(A[u]);
if(sptr != small.end()){
small.erase(sptr);
}else{
auto lptr = large.find(A[u]);
assert(lptr != large.end());
large.erase(lptr);
}
balance(small,large);
return res;
}

int main(){
int n = read();
rep(i,1,n+1) A[i] = read();
rep(i,1,n){
int u = read();
int v = read();
e[u].pb(v);
e[v].pb(u);
}
maxSet<int> small ; // 前一半
minSet<int> large ;
printf("%d\n",dfs(1,1,small,large,0));
return 0;
}

H - Red and Blue Lamps

https://atcoder.jp/contests/abc218/tasks/abc218_h

N个灯, 你需要让R个红色,N-R个蓝色

如果 i 和 i+1 不同色 则有Ai的贡献

求最大的贡献

范围

N 2e5

R [1,N-1]

Ai [1,1e9]

2s

1024mb

我的思路

显然有个n^2的dp

dp[i][j][c] = 前i个有j个红色,第i个颜色为c,的最大贡献

dp[i][j][red] = max(dp[i-1][j-1][red] , dp[i-1][j-1][blue] + A[i])

dp[i][j][blue] = max(dp[i-1][j][red] +A[i], dp[i-1][j][blue])

但肯定超时

题解

如果红色比蓝色多, 则交换颜色数量

那么尽量多的两两不同,显然红色不相邻

那么把 A[i]+A[i+1] 看作整体

构造B[i] = A[i]+A[i+1] 数组

变成在B中选r个不相邻的元素使得总价值最大, 类似的dp[i][j][0/1] = 前i个,选了j个,第i个是否选的最大值

dp[i][j][1] = dp[i-1][j-1][0] + B[i]

dp[i][j][0] = max(dp[i-1][j][1],dp[i-1][j][0])

复杂度依然不能接受


但是有简单的性质: 可以王钦石二分

王钦石二分

  1. 当可选的越多(虽然题目要你求具体的), 那么总收益越大(单增)
  2. 当选的越多, 增量非严格递减凹函数(上凸)
  3. 不限制个数,容器得到最优方案

反过来, 如果我们指定一个最大收益, 那么可以快速算出需要可选的最少数量

如果变成二维图,是凹(上凸)函数,

方法是二分斜率, 对斜率找和凹函数的切点(切线)

而显然切线在y轴截距最大, f(x) = g(x) - k x, 原函数g(x), 截距函数f(x)

问题变成 给k, 找最大f(x), 而 g(x) - k x 从另一个角度看, 就是每个值-k 以后选x, 对于x没有限制时, 容易求的话,那就容易得到f(x) 和 x

求的话因为干掉了一个记录当前有多少个的限制, 从而可以简单dp


然后…. 我被卡double /long double 了

现实是,本身斜率就是 每次增量, 答案不会有小数,而切点对切线斜率也是单调影响, 所以,全整数就行了

代码

https://atcoder.jp/contests/abc218/submissions/33768310

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
#define rep(i,a,n) for (ll i=a;i<(ll)n;i++)

ll read(){ll r;scanf("%lld",&r);return r;} // read

int n;
ll a[200010];

pair<ll,int> f(ll v){ // f(斜率) => {最大截距, 横坐标(选的个数)}
auto dp = vector(n,vector(2,pair<ll,int>{0,0})); // dp[i][第i个 选/不选] = {最大截距,个数}
rep(i,1,n){
auto [y0, c0] = dp[i-1][0];
dp[i][1] = {y0 + (a[i-1] - v), c0 + 1}; // 当前选, 则上一个不选
dp[i][0] = max(dp[i-1][0],dp[i-1][1]); // 当前不选, 则上一个可选可不选
}
return max(dp[n-1][0],dp[n-1][1]);
}

int main(){
n = read();
int x = read();
x = min(x, n-x);
rep(i,0,n-1) a[i] = read(); // [0..n-2]
rep(i,0,n-1) a[i] += a[i+1]; // [0..n-2], 看成多了末尾多了一个0, 对最大值无影响
ll L = 0, R = 3000'000'000; // 斜率
// 二分, 这里可能 有多个点 让同一个斜率最大, 保证 斜率R < x <= 斜率L 即可, 注意到这这里可能 f(R).pos < f(L).pos < x, 但f(L).pos 只是斜率L上的任意一点 但只要x 斜率不大于 L即可
while(L + 1 < R) (f((L+R)/2).second < x ? R : L) = mid;
printf("%lld\n", f(L).first + x * L);
}

总结

G

没啥难的

H

感觉有的时候还是有点 PE的味道,偏数学一点

这里涉及 王钦石二分, 也是凸(凹)函数和二分相关的知识

然后这里double还不行,得long double

参考

官方题解

王钦石二分

G - Groups

https://atcoder.jp/contests/abc217/tasks/abc217_g

有数字1..N

把它们分成k组(每组至少一个数)

要求, 每组中没有两个数 mod M 是相等的

问对于k=1…n 分别有多少方案

答案 模 998244353

方案: 如果两方案不同,至少有一个(x,y) 在一个中同组,另一个是不同组

范围

n 5000

2 <= m <= n

我的思路

显然 (n - 1) / m + 1 个 mod m = 1 的

我们也容易计算mod m = r 的有多少个

然后 计算这个方案数无非是两个方向, 正向算和逆向算

正向算, 则需要对每个方案唯一标识, 那么考虑用每组中( (value - 1 )mod m, value) 最小的作为组标识

似乎相互依赖很多, 不知道怎么算

反向算, 则就是同 mod的放在同一个位置了

题解

我感觉自己已经傻掉了, 看到n 5000 竟然没有想一下n方的算法

dp[i][j] 表示前i个数,分成了j组方案

如果没有同余限制

dp[i][j] = dp[i-1][j-1](单独放在j) + j * dp[i-1][j] (前面i-1已经放了j)

注意到同余的限制

那么j的放法就是 和它不同余的位置

已经有 ((i-1) / m)个和它同余了

dp[i][j] = dp[i-1][j-1](单独放在j) + (j-(i-1)/m)保证非负 * dp[i-1][j] (前面i-1已经放了j)


就没了

代码

https://atcoder.jp/contests/abc217/submissions/33750183

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <bits/stdc++.h>
#include <atcoder/modint>
using mint = atcoder::modint998244353;
using namespace std;

#define rep(i,a,n) for (int i=a;i<(int)n;i++)
int read(){int r;scanf("%d",&r);return r;} // read

int main() {
int n = read();
int m = read();
vector<vector<mint>> dp(n + 1, vector<mint> (n + 1));
dp[0][0] = 1;
rep(i,1,n+1){
rep(j,1,i+1) { // j-(i-1)/m >= 0
dp[i][j] = dp[i-1][j-1];
if(j-(i-1)/m >= 0) dp[i][j] += dp[i-1][j] * (j-(i-1)/m);
}
}
rep(i,1,n+1) printf("%d\n",dp[n][i].val());
return 0;
}

H - Snuketoon

https://atcoder.jp/contests/abc217/tasks/abc217_h

初始在点0, 每秒可以-1,0,+1

N次事件: 在ti时刻, 若在Xi左侧且Di = 0,受到和Xi距离的伤害, 若在Xi右侧, 且Di = 1,受到和Xi距离的伤害

想要受到伤害最小化, 求最小值

范围

N 2e5

ti [1,1e9]

di 0/1

Xi [-1e9,1e9]

我的思路

看起来像dp

题解

$dp_{i,x} = $ 在Ti分钟 恰好在x 所需要受到的最小伤害, 为了方便认为T0 = 0

那么

dp[0][0] = 0

dp[0][!=0] = INF

若Di = 0, dp[i][x] = min(dp[i-1][y]) + max(0,Xi - x), 且|y-x| <= T[i] - T[i-1]

若Di = 1, dp[i][x] = min(dp[i-1][y]) + max(0,x - Xi), 且|y-x| <= T[i] - T[i-1]

很明显直接算会TLE


对于一个具体的i, 在二维平面上画点$(x,dp_{i,x})$, 其中横坐标范围是$[-T_i,T_i]$, 然后用线段连起来, 发现是个凸函数(下凸)

证明: 若对于i-1是下凸函数, 注意到min(dp[i-1][y]) 依然是凸函数, 而max部分也是凸函数,所以对于i 也是凸函数


因此问题变成维护那些拐点

每次min的操作,相当于把最小值的区间 向两侧 平移T[i]-T[i-1]

注意到 最初是[0,0], 其实就是把区间最左和最有移动到-Ti和正Ti

然后每次+max, 相当于一段不变, 一段 斜率+1, 还可能产生新的节点

如何维护呢?

注意到每次 min的过程核心等于向两侧平移, 如果以 和-Ti,Ti的距离来看, 甚至是没有变化

每次 +max, 是区间内斜率增加1, 那不妨直接记录 斜率1的起始点,斜率2的起始点, 斜率3的起始点( 可能会有重合, 这些点和左侧的距离, 和右侧的距离


-3 -2 -1 0 1 2 3 这样的斜率区间

如果加上 0 1 的

最终会变成 -3 -2 -1 0 1 2 3 4 这样的, 虽然可能有起点重合(斜率区间长度为0

代码

https://atcoder.jp/contests/abc217/submissions/33754555

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
#define rep(i,a,n) for (ll i=a;i<(ll)n;i++)

ll read(){ll r;scanf("%lld",&r);return r;} // read

priority_queue<ll> ql; //大根堆,左侧, 记录斜率-1的起点,-2的起点,-3的起点,...到 -Ti的距离
priority_queue<ll> qr; //大根堆,右侧, 记录斜率 1的起点, 2的起点, 3的起点,...到 Ti的距离

int main() {
int n = read();
ll ans = 0;
rep(i, 1, n+1) {
ll t = read();
ll d = read();
ll x = read();
// 让下凸的最小值一直是0
if (d == 0) {
if (x > t) { // 超出范围直接全部都加上
ans += x - t;
x = t;
}
if (qr.empty() || qr.top() <= t-x) { // 不影响右侧的
ql.push(x + t);// 原来-1变-2, -2变-3,... , 新的-1的起点
} else { // ... -3 -2 -1 0 ... 0 1 2 3 ... 影响右侧的,
int pos = t - qr.top(); // 最小值的位置 右侧1斜率的起点
qr.pop();
ans += x - pos; // 让下凸的最小值 = 0
ql.push(pos-(-t)); // 成为左侧新的 -1 斜率的起点
qr.push(t - x); // 插入新的 斜率变化分割(距离右侧Ti
}
} else { // d = 1
if (x < -t) { // 超出范围直接全部都加上
ans += -t - x;
x = -t;
}
if (ql.empty() || x - (-t) >= ql.top()) { // 不影响左侧
qr.push(t - x); // 右侧最大点
} else {
int pos = ql.top() - t; // 最小值的位置, 左侧-1斜率的起点
ql.pop();
ans += pos-x;
qr.push(t-pos);
ql.push(x-(-t));
}
}
}
printf("%lld\n", ans);
return 0;
}

总结

G

干啊, n方dp都想不到了,我

据说有数学的 N log N 的方法

H

知识点就是凸函数, 但是变化点较少时, 只需要维护这些点即可

然后维护的时候, 想办法保持尽可能多的不变量, 让变化记录是常数级别

参考

官方题解

Layout

http://poj.org/problem?id=3169

数轴上放N个点(按照i的顺序坐标非严格单调递增

10000 个大于限制, 点i和点j距离不超过 di (1e6)

10000 个小于限制, 点i和点j距离不小于 di (1e6)

1s

64MB

求点1 和 点N的最大距离

题解

传说中日本众所周知的的cow game

也就是 全部小于号, (大于号同乘-1)

注意到上面要保证i的顺序( 所以 大-小 <= Di 或者 大减小 >= Di

然后说 差分约束本质上还是 最短路 只是需要建图

b-a <= d

转化成 a + d >= b, 所以 a -> b如果有边长d的话, 那么b的距离最小就是 a+d 还可能更小

b-a >= d的话

转化成 b-d >= a, 也就是 b -> a 如果有边长 (-d), 那么a的距离最小是 b-d, 还可能更小

总而言之转化成

点0 + ? >= 点1 的形式, 然后从点0 发一条长? 的边

代码

过不了编译版本

这poj g++版本太老了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
#define rep(i,a,n) for (int i=a;i<n;i++)

ll read(){ll r;scanf("%lld",&r);return r;} // read
const int INF = 0x3f3f3f3f; // 1e9

vector<array<int,3> > e; // 有向边 [u,v,weight/length]

int main(){
int N = read();
int ML = read();
int MD = read();

// 建图
rep(i,1,N) e.push_back({i+1, i, 0}); // 从i+1->i权值为0的边 保证顺序i的距离不大于i+1
rep(i,0,ML){ //'喜欢'关系约束 <= DL
int a = read();
int b = read(); // 保证 b>a
int d = read(); // |距离| <= DL[i]
/* b-a<=d, 即 a+d>=b 从a到b权值为d的边 */
e.push_back({a,b,d});
}
rep(i,0,MD){ //'不喜欢'关系约束 >= DD
int a = read();
int b = read(); // 保证 b>a
int d = read(); // |距离| >= DD[i]
/* BD - AD >= DD 即 BD - DD >= AD, 从BD到AD权值-DD的边*/
e.push_back({b,a,-d});
}

// Bellman_Ford
vector<int>dist(N+1,INF);// 最短距离
dist[1] = 0;
//循环N-1次 对所有边进行松弛操作
rep(i,0,N-1) for(auto [u,v,w]: e) if(dist[u] != INF) dist[v] = min(dist[v], dist[u] + w);

//再遍历一次所有边(第N次循环)如果本次有更新,则存在负环
bool ngloop = false;
for(auto [u,v,w]: e) if(dist[u] != INF && dist[u] + w < dist[v] ) ngloop = true;
int ans = dist[N];
if (ngloop) ans = -1;
else if(ans == INF) ans = -2;
printf("%d\n", ans);
return 0;
}

AC 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <stdio.h>
#include <iostream>
#include <vector>
using namespace std;

typedef long long ll;
#define rep(i,a,n) for (int i=a;i<n;i++)
#define mt make_tuple

ll read(){ll r;scanf("%lld",&r);return r;} // read
const int INF = 0x3f3f3f3f; // 1e9

// 没有tuple
struct edge{
int u;
int v;
int w;
// 结构体直接赋值也没有
edge(int _u,int _v,int _w){
u = _u;
v = _v;
w = _w;
}
};

vector<edge> e; // 有向边 [u,v,weight/length]

int main(){
int N = read();
int ML = read();
int MD = read();

// 建图
rep(i,1,N) e.push_back(edge(i+1, i, 0)); // 从i+1->i权值为0的边 保证顺序i的距离不大于i+1
rep(i,0,ML){ //'喜欢'关系约束 <= DL
int a = read();
int b = read(); // 保证 b>a
int d = read(); // |距离| <= DL[i]
/* b-a<=d, 即 a+d>=b 从a到b权值为d的边 */
e.push_back(edge(a,b,d));
}
rep(i,0,MD){ //'不喜欢'关系约束 >= DD
int a = read();
int b = read(); // 保证 b>a
int d = read(); // |距离| >= DD[i]
/* BD - AD >= DD 即 BD - DD >= AD, 从BD到AD权值-DD的边*/
e.push_back(edge(b,a,-d));
}

// Bellman_Ford
vector<int>dist(N+1,INF);// 最短距离
dist[1] = 0;
//循环N-1次 对所有边进行松弛操作
rep(t,0,N-1) rep(i,0,e.size()){
edge &uvw = e[i]; // 不支持auto
int u = uvw.u;
int v = uvw.v;
int w = uvw.w;
if(dist[u] != INF) dist[v] = min(dist[v], dist[u] + w);
}

//再遍历一次所有边(第N次循环)如果本次有更新,则存在负环
bool ngloop = false;
rep(i,0,e.size()){
edge &uvw = e[i]; // 不支持auto
int u = uvw.u;
int v = uvw.v;
int w = uvw.w;
if(dist[u] != INF && dist[u] + w < dist[v] ) ngloop = true;
}
int ans = dist[N];
if (ngloop) ans = -1;
else if(ans == INF) ans = -2;
printf("%d\n", ans);
return 0;
}

总结

就是如何对差分变成图

G - 01Sequence

https://atcoder.jp/contests/abc216/tasks/abc216_g

代码

https://atcoder.jp/contests/abc216/submissions/33727628

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <bits/stdc++.h>
using namespace std;

#define rep(i,a,n) for (int i=a;i<(int)n;i++)

int read(){int r;scanf("%d",&r);return r;} // read

const int N=200000;
int a[N+10]; // a[空白个数] = 到右侧点, 之间全是1
int r[N+10]; // 读入
int y[N+10]; // 左侧0个数
vector<int> l2i[N+10]; // 左端点到第i个区间

int main() {
int n = read();
int m = read();
rep(i,1,m+1){
int l = read();
r[i] = read();
y[i] = (r[i]-l+1) - read(); // 左侧0个数 [[....yi],1,1,1,1,1,1]
l2i[l].push_back(i);
}
int cnt = 0; // 遍历过程中 (<l) 0 的个数
rep(pos,1,n+1){ // 下标
for(auto i:l2i[pos]) a[y[i]+cnt] = max(a[y[i]+cnt],r[i]);// [pos.....r[i]]
printf("%d ", a[cnt] >= pos); // 这一段全是1, 1尽量向右,贪心塞0
cnt += (a[cnt] < pos); // 计数+1
}
return 0;
}

H - Random Robots

数轴上k个机器人, 初始位置分别在xi

每次 每个机器人独立选择 移动(正向+1)或不动 1/2 概率

问经过N次,过程中没有任何两个robot 同时在同位置的概率

mod 998244353

限制

k [2,10]

n 1000

xi [0,1000], 严格单增提供

2s

1024mb

我的思路

一般来说 概率 = 次/总次数 可以互相转化

不相遇 可以 和相遇的容斥互相转化

k 10 的话可能和k的bitmask有关系

如果进行一次

而碰撞比不碰撞似乎好算一些

而且一般是相邻碰撞

pi 和pi+1 在t次时刻碰撞

意味着 t-1 次时距离1, t时 1/4 概率

0~t-1 时刻每次 1/4 +1, 1/4 -1, 1/2 不变

设原来距离 为d

那么 -1 次数 减去 +1 次数 = d-1, 且中间不能有负数情况

变成后缀个数统计问题

似乎可以强行算出t时刻 的概率, 实在组合排列不行, dp[时刻1000][距离2000] 来算也可以


那么无碰撞 = 所有 - 碰撞

所以想办法容斥掉碰撞

题解

用一下LGV引理相关的思路: 相交的路径 总有转化方法,成对的出现互为相反数的贡献,从而有相交的内容贡献和为0

每一个路径组方案贡献1 乘上-1的最终位置的逆序列数量次方, 其实就像当于LGV中所有边权为1 的特殊情况

$\sum_{Q} (-1)^{\sigma(Q)}\cdot(\frac{1}{2})^{NK}\cdot\prod_{i=1}^K {\rm C}(N,Q_i-x_i)$

也就是 方案 * (-1) 的幂次权, 再除以总方案数

Qi 为初始第i个机器人最终的下标

$\sigma(Q)$ 为逆序对个数

那么对于一条具体的有交的路径, 找其编号最小交点, 其中最小的起始位置,做后置路径交换(和LGV一样), 那么将得到一个新的路径组,有同样的交点,最小交点的最小起始位置依然相同, 但逆序对数变化为1, 所以总贡献为0


f[S][j] = 选起点集合在S中, 最终节点最大值 <= j 的 带权 方案数和

ans = f[{1,...,k}][x[k] + n]

考虑状态转移

最终最大节点 < j, f[S][j] += f[S][j-1]

最终最大节点 = j, f[S][j] += lgv中的行列式值 展开最后一列

所以有

$f(S, j) = f(S, j-1) + \sum_{i=1}^{|S|} (-1)^{|S|+i} e(x_{s_i}, j) f(S\setminus{s_i}, j-1).$

$f(S, j) = f(S, j-1) + \sum_{i=1}^{|S|} (-1)^{|S|+i} \binom{n}{j-x_{s_i}} f(S\setminus{s_i}, j-1).$

状态$2^k(n + x_k - x_1)$, 转移倍数$k$

总时间复杂度 $2^kk(n + x_k - x_1)$


注意到j仅依赖于j-1, 所以可以滚动数组降低空间

而S依赖于的都是S子集, 所以保证顺序即可

$for(j) \\ f(S) = f(S) + \sum_{i=1}^{|S|} (-1)^{|S|+i} \binom{n}{j-x_{s_i}} f(S\setminus{s_i}).$

注意到这里的i不是X数组的i而是X选出的x按照顺序组成的S中的i, 且是1-index

也可以表示成$d(S,i) = S$中比$i$大的的个数

$for(j) \\ f(S) = f(S) + \sum_{i=1}^{|S|} (-1)^{d(S,i)} \binom{n}{j-x_{s_i}} f(S\setminus{s_i}).$

代码

https://atcoder.jp/contests/abc216/submissions/33737388

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <bits/stdc++.h>
#include <atcoder/modint>
using mint = atcoder::modint998244353;
using namespace std;

typedef long long ll;
#define rep(i,a,n) for (ll i=a;i<(ll)n;i++)
#define per(i,a,n) for (ll i=n;i-->(ll)a;)

ll read(){ll r;scanf("%lld",&r);return r;} // read

int x[2010]; // 初始位置
mint f[2010] = {1}; // f[i] = binom(n,i)
int p[(1<<10)+10]; // p[mask] = (-1)^(mask中1的个数)
mint dp[(1<<10)+10] = {1}; // 第二维滚动 f(S,pos) = f(S, pos-1) + \sum_{i=1}^{|S|} (-1)^{count(S,> i)} \binom{n}{pos-x_{s_i}} f(S\setminus\{s_i\}, pos-1).$

int main() {
int k=read();
int n=read();
rep(i,0,k) x[i]=read();
rep(i,1,n+1) f[i]=f[i-1]*(n-i+1)/i; // binom(n,i-1) -> binom(n,i)
rep(mask,0,1<<k) p[mask] = p[mask>>1] * (mask&1?-1:1);
rep(pos,x[0],x[k-1]+n+1){ // 第二维滚动
per(mask,0,1<<k) { // 第一维 bitmask 注意顺序
rep(i,0,k) if(mask&(1<<i)) { // 变成递推贡献, 要增加的bit位
if(x[i]<=pos && pos<=x[i]+n) { // 保证 binom 不为0
// f(S) += f(S\i) * binom(n, pos - x[S_i]) * (-1)^count(S,>i)
dp[mask] += dp[mask^(1<<i)] * f[pos-x[i]] * p[mask>>(i+1)];
}
}
}
}
printf("%d\n",(dp[(1<<k)-1] / ((mint)2).pow(k*n)).val()); // 频次/总次数 = 概率
return 0;
}

总结

G

贪心完全不会

题解说有个cow game

有一些 dj-di <= wij 的限制

寻找最大的 dT-dS, 可以变成最短路问题

http://poj.org/problem?id=3169

H

学了一下LGV引理, 和其思路细节

路径不相交问题首选逆序对容斥,那么可以套用 LGV 引理

相关练习: https://www.luogu.com.cn/problem/P7736

参考

官方题解

LGV引理

G - Colorful Candies 2

N 个 有色糖果,第i个颜色c[i]

从中选K个有 binom(N,K)种方案

等概率选一种方案

价值=所选的颜色的不同的数量

对于每个 k= 1…N 求期望价值

mod 998244353

限制

N 5e4

c[i] [1,1e9]

4s

1024mb

我的思路

首先只关心不同值,显然c[i]可以离散化到[1..N]

答案 = sum{价值} / binom(N,K)

不同颜色互不影响

所以 选了j种颜色, 一共k个, 如果能算方案出来 f(j), 那么答案 = sum j * f(j)

指定的 c[…] 中选的话

似乎可以卷积

去表示每个颜色 选t个的方案数, 然后卷积意义是前 j 种颜色(可能有的不选) 选k个的方案数

好像无法获得选了几个颜色

题解

反过来, 也就是每个颜色出现一次的概率 的贡献和

P(出现一次) = 1-P(一次都不出现)

binom(n-x,k) / binom(n,k) , 也就是x 是这个颜色的个数

其中 n-x < k 的话, 必定出现p = 1

(binom(n,k) - binom(n-x,k))/binom(n,k) , 也就是x 是这个颜色的个数

可以减少计算

注意到 可以统计个数为x的有多少个, 这样最多 $\sqrt(N)$个统计

因此对于k来讲,是$O(\sqrt{N})$的

总的便是$O(N^{\frac{3}{2}})$

代码

https://atcoder.jp/contests/abc215/submissions/33712411

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <bits/stdc++.h>
#include <atcoder/all>
using mint = atcoder::modint998244353;
using namespace std;

typedef long long ll;
const int MOD = 998244353;
#define rep(i,a,n) for (ll i=a;i<(ll)n;i++)
#define per(i,a,n) for (ll i=n;i-->(ll)a;)
#define pb push_back

ll read(){ll r;scanf("%lld",&r);return r;} // read

int c[50010];
mint fac[50010] = {1};
mint invv[50010] = {0,1};
mint invfac[50010] = {1};

mint binom(int n,int m){
if(m > n) return 0;
return fac[n] * invfac[m] * invfac[n-m];
}

int main(){
int n = read();
rep(i,0,n) c[i] = read();

rep(i,1,n+1) fac[i] = fac[i-1] * i;
invv[1] = 1;
rep(i,2,n+1) invv[i] = (MOD - MOD/i) * invv[MOD%i];
rep(i,1,n+1) invfac[i] = invfac[i-1] * invv[i];

sort(c,c+n);
vector<int> sz = {1};
rep(i,1,n){
if(c[i] != c[i-1]){
sz.push_back(1);
}else{
sz.back()++;
}
}
sort(sz.begin(),sz.end());
vector<pair<int,int>> sc; // size count
int cnt = 0;
rep(i,0,sz.size()){
cnt++;
if(i+1 == (int)sz.size() || sz[i] != sz[i+1]){
sc.push_back({sz[i],cnt});
cnt = 0;
}
}

rep(k,1,n+1){
mint bnk = binom(n,k);
mint ans = bnk * sz.size() ;
for(auto [s,t]:sc) {
if(n-s < k) break; // n-s < k 的话, 必定出现p = 1
ans -= binom(n-s,k) * t; // * 次数
}
ans /= bnk;
printf("%d\n",ans.val());
}
return 0;
}

G - Cabbage Master

N种菜,每种 A[i] 个

M个需求, 每个需求B[i] 个, 但是限制c[i][j] = 0/1 表示第i个需求 是否允许得到 第j种菜

如果 能满足所有需求则 成功

现在要尽量少的删除一些菜的个数, 让它无法成功

并且求, 删除同样数量的方案数 mod 998244353

限制

n 20

m 1e4

a[i] 1e5

b[i] 1e5

3s

1024mb

我的思路

一眼感觉网络流, 但是看着这n这么少

又觉得说 会不会是 maskdp

2^20 = 1048576, 大概1e6


网络流思路

就是 S -> Ini 流量 A[i]

Ini -> Outj 流量无限 如果c[i][j] == 1

Outj -> T 流量B[i]

那么目标是让最小割(最大流) <= sum B[i] 即可

题解

二分图

左侧N个颜色, 右侧M个需求

注意到 这里成功对应的是 匹配 = sum 右侧

所以是枚举右侧的的点集,看对应左侧的点集是否大于等于右侧

左侧L, 右侧R

要能完美匹配 $\forall S \subset R $

左侧对应集合并的和 $\ge S$对应需求的和

即 min (左侧对应集合并的和 - S对应需求的和) >= 0


n=20, 考虑枚举左侧的并,来找右侧的max

但似乎通过枚举子集可能有m 2^n 复杂度

但实际上我们要的是

min {f(L0) - g(L0)} >= 0, 其中f 算的集合里左侧的和, g 算的映射到左侧包含于集合的右侧的值的和 (既然B[i] 都是正的,那就是所有的加起来让g达到最大

g中计算 子集和可以sosdp 高维前缀和

那么删除数量X = min( f(L0) - g(L0)) + 1


然后问题变成如何计算方案数

ans = 0 , 不操作1种

对于ans > 0

设 左侧移除的X的值 来自的点集合恰好为S

当存在 S1 满足 S 是 S1 的子集, 且 f(L0) - g(S1) + 1 == X, 这时 S移除X的方案数h(S) 会有贡献

binom(S的个数,X) = sum h(T), T 是S的子集

这就是 子集反演问题

$h(S) = \sum (-1)^{|S|-|T|} binom(T的个数,X)$, T是S的子集

又是求子集的函数和, 那么这里把和原集合有关的移动一下

$(-1)^{|S|} {h(S)} = \sum (-1)^{|T|} \binom{f(T)}{X}$, T是S的子集

同样FWT, SOSDP可以处理

子集反演

$g(S) = \sum f(T)$, T是S的子集合

$f(S) = \sum (-1)^{|S| - |T|} g(T)$ , T是S的子集合

代码

https://atcoder.jp/contests/abc215/submissions/33719233

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <bits/stdc++.h>
#include <atcoder/all>
using mint = atcoder::modint998244353;
using namespace std;

typedef long long ll;
#define rep(i,a,n) for (ll i=a;i<(ll)n;i++)
#define per(i,a,n) for (ll i=n;i-->(ll)a;)

ll read(){ll r;scanf("%lld",&r);return r;} // read

const ll INF = 0x3f3f3f3f; // > 1e9
const int N = 20;
const int MASK = ( 1 << N ) ;
const int M = N * 100000;

mint fac[M+10] = {1}; // 阶乘
mint ifac[M+10] ={1}; // 阶乘的逆向

int f[MASK+10]; // f[1 << i] = A[i], f[mask] = sum A
int g[MASK+10]; // g[左侧mask] = sum B , 通过sosdp 变成子集和
mint h[MASK+10]; // h(S=bitmask) S中移除 X 个的方案数,(每个恰好一个)
int cnt[MASK+10]; // mask 中1的个数
int cont[MASK+10]; // cont[mask] = 多少个父集合 是满足 最小代价为X的 在mask中移除让Hall定理触发不满足

int B[M+10]; // 读入
int adj[M+10]; // adj[右侧] = 左侧的bitmask

mint binom( int n, int m ) { return n < m ? 0 : fac[n] * ifac[m] * ifac[n - m]; }

int main() {
rep(i,1,M+1) fac[i] = fac[i - 1]*i;
ifac[M] = fac[M].inv();
per(i,0,M) ifac[i] = ifac[i + 1]*(i + 1);
int n = read();
int m = read();
rep(i,0,n)f[1 << i]=read();//f[1 << i]=A[i]
rep(j,0,m)B[j]=read();
rep(i,0,n)rep(j,0,m) if(read())adj[j]|=1 << i; // 转换成mask
rep(j,0,m)g[adj[j]] += B[j]; // g[对应左侧mask] += B[j]
rep(mask,1,1 << n) cnt[mask] = cnt[mask >> 1] + ( mask & 1 ); // 计算1个数
rep(mask,1,1 << n) f[mask] = f[mask&(-mask)] + f[mask&(mask-1)]; // 去掉最后一个1 和最后一个1的mask之和
rep(pwr,0,n) rep(mask,1,1 << n) if(mask & (1 << pwr)) g[mask] += g[mask ^ (1 << pwr)]; // sosdp 高维前缀和
rep(mask,0,1 << n) if(!g[mask]) g[mask] = -INF; // 右侧没有集合对应左侧集合是mask的子集合
int X = INF; // 答案第一部分 删除个数
rep(mask,1,1 << n) X = min(X,f[mask]-g[mask]+1 ); // Hall定理, min(左子集值和 - 右侧来源子集和)
if(max(X,0) == 0) { // 本来就不合法
printf( "0 1\n" );
return 0;
}
// h'(S) = (-1)^|S| h(S) = sum (-1)^|T| binom(f[t], X) SOSDP
rep(mask,1,1 << n) h[mask] = binom(f[mask], X) * (cnt[mask] & 1 ? -1 : 1);
rep(pwr,0,n) rep(mask,0,1 << n) if(mask & (1 << pwr)) h[mask] += h[mask ^ (1 << pwr)];
rep(mask,0,1 << n) h[mask] = h[mask] * ((cnt[mask] & 1)?-1:1);

// SOSDP 父集合反演(本质上还是 子集反演, 你只是把每个mask 看成mask的取反即可, 最外层是pwr顺序, mask每次之间没有链式依赖,都是两两依赖, 所以mask不需要换顺序
rep(mask,1,1 << n) if( f[mask] - g[mask] + 1 == X ) cont[mask] = 1;
rep(pwr,0,n) rep(mask,0,1 << n) if(mask & (1 << pwr)) cont[mask ^ (1 << pwr)] += cont[mask]; // 统计父集合可行的次数

mint ans = 0;
rep(mask,0,1 << n) if(cont[mask]) ans += h[mask];
printf("%d %d\n",X, ans.val());
return 0;
}

总结

G

其实还算基础知识点, 如何批量算模拟元,批量阶乘和阶乘模逆元,如何基于它们快速bionom

然后就是概率统计变形状和相同次数统计变成$\sqrt{N}$

评分 2267 也差不多

H

二分图,霍尔(Hall)定理

二分图一定程度上, 就不在意初始设计的方向了, 因为是匹配, 内部是没有关系的

霍尔定理对于这种大于1流量的也适用(因为从本质上看 左/右侧最多k个, 无非是k个点, 有无穷大边, 无非是这些拆开后的左右侧按照原来的关系两两有边, 而这个思路是不是特殊题型还能反过来思考)

这种”任意”的条件,可以考虑是在哪个部分将它破坏的

还涉及到 子集反演(以及用子集反演完成父集合反演)

参考

官方题解

hall’s theorem

0%