第46届ICPC东亚洲区域赛(昆明)补题题解
第46届ICPC亚洲区域赛(昆明)(正式赛)
本博客是补题向题解,仅做记录方便自己理解。会参考大佬们的博客,所以代码会很像,会在题目前贴出参考的博客出处。
B-Blocks
本题参考知乎大佬cup-pyy的题解知乎 cup-pyy
题意
T T T组输入,给定一个大矩形,以左上角,右下角坐标 ( x 1 , y 1 ) , ( x 2 , y 2 ) (x1,y1),(x2,y2) (x1,y1),(x2,y2)的形式给出 ( 0 , 0 ) , ( W , H ) (0,0),(W,H) (0,0),(W,H),以及 n n n个小矩形。一次操作会等概率的染黑任意一个小矩形(可能会重复染),问将大矩形染黑的操作次数的期望,如果无论如何也不能染黑输出 − 1 -1 −1。 T < = 500 , 1 < = n < = 10 T<=500 ,1<=n<=10 T<=500,1<=n<=10 ,矩形的坐标 0 < = x 1 , x 2 , y 1 , y 2 < = 1 e 9 0<=x1,x2,y1,y2<=1e9 0<=x1,x2,y1,y2<=1e9.
解析
对于求解期望我们需要解决一个问题,即最终态染黑大矩形可以由哪些小矩形染黑组成我们还是未知,对于本题的一个重点(如何预处理出最终状态)可以参考cup-pyy大佬的博客,对于处理的方式以及时间复杂度的分析优化已经讲解的很好,本人的代码也会有相应注释在这里就不赘述。
值得一提的是,在矩形范围很大,但矩形数量很小的情况下,离散化后将矩形分为一个个小方格再用bitset将二维的矩形压缩成一维来降低复杂度的方式真的很妙。
例如:
100
100
100
转化为bitset表示:100100100
我们详细讲解的是如何得到递推方程:
在预处理出哪些是最终态以后,我们可以用二进制数来代表这些状态,数的二进制位上为
1
1
1就代表该编号的矩形被染黑。因为只有
10
10
10个矩形,我们可以采用状态的DP的方式逆向递推求解(从最终态推到全空白的情况)。
定义
f
[
i
]
:
f[i]:
f[i]:从状态
i
i
i到达最终态的期望次数,(最终态的
f
[
i
]
=
0
f[i] = 0
f[i]=0)。
这里我们可以使用经典的设未知数求解的方法求解
设当前状态是
i
i
i,分为两种情况
1.
1.
1.下一次操作染黑编号为
j
j
j的矩形并不包括在状态
i
i
i中,
2.
2.
2.染黑编号为
k
k
k的矩形是已经在状态
i
i
i中染黑的矩形。
1.
∑
j
=
1
n
1
n
(
f
[
i
∣
(
1
≪
j
)
]
+
1
)
1.\sum_{j=1}^{n}\frac{1}{n}(f[i \mid (1\ll j)] + 1)
1.∑j=1nn1(f[i∣(1≪j)]+1)(
j
j
j为
i
i
i中没有的状态)
2.
∑
k
=
1
n
1
n
(
f
[
i
]
+
1
)
2.\sum_{k=1}^{n}\frac{1}{n}(f[i ] + 1)
2.∑k=1nn1(f[i]+1)(
k
k
k为
i
i
i中已有的状态)
于是有:
f
[
i
]
=
∑
j
=
1
n
1
n
(
f
[
i
∣
(
1
≪
j
)
]
+
1
)
+
∑
k
=
1
n
1
n
(
f
[
i
]
+
1
)
f[i] = \sum_{j=1}^{n}\frac{1}{n}(f[i \mid (1\ll j)] + 1) + \sum_{k=1}^{n}\frac{1}{n}(f[i ] + 1)
f[i]=∑j=1nn1(f[i∣(1≪j)]+1)+∑k=1nn1(f[i]+1)
因为我们是逆推的,最终态的
f
[
i
]
=
0
f[i] = 0
f[i]=0已知,所以上式中的未知数只有
f
[
i
]
f[i]
f[i]求解即可。
递推到最后
f
[
0
]
f[0]
f[0]就是答案。
代码
#include <bitset>
#include <iostream>
#include <algorithm>
using namespace std;
#define ll long long
const int N = 500, mod = 998244353;
ll ksm(ll a,ll b){
ll ans = 1;
while(b){
if(b & 1) ans = ans * a % mod;
a = a * a % mod;
b >>= 1;
}
return ans % mod;
}
struct Rec{
int x1,y1,x2,y2;
}r[15];
int n,h,m,s;
bitset<N>msk[11]; // 将二维降维到一维,方便维护
int row[25],col[25],cntx,cnty;
ll f[1 << 11],inv[25];// dp 和 逆元
void solve()
{
scanf("%d%d%d",&n,&h,&m);
cntx = cnty = 0;
for(int i = 1; i <= n; i ++){
scanf("%d%d%d%d",&r[i].x1,&r[i].y1,&r[i].x2,&r[i].y2);
r[i].x1 = min(r[i].x1, h);
r[i].y1 = min(r[i].y1, m);
r[i].x2 = min(r[i].x2, h);
r[i].y2 = min(r[i].y2, m);
row[++ cntx] = r[i].x1, row[++ cntx] = r[i].x2;
col[++ cnty] = r[i].y1, col[++ cnty] = r[i].y2;
}
//离散
sort(row + 1,row + 1 + cntx);
sort(col + 1,col + 1 + cnty);
cntx = unique(row + 1,row + 1 + cntx) - (row + 1);
cnty = unique(col + 1,col + 1 + cnty) - (col + 1);
s = (cntx - 1) * (cnty - 1); // 被划分成的块数
bitset<N>tmp;
for(int i = 1; i <= n; i ++){// 预处理每一个小矩形在离散化后的方格中占的块
int x1 = lower_bound(row + 1,row + 1 + cntx, r[i].x1) - row;
int y1 = lower_bound(col + 1,col + 1 + cnty, r[i].y1) - col;
int x2 = lower_bound(row + 1,row + 1 + cntx, r[i].x2) - row;
int y2 = lower_bound(col + 1,col + 1 + cnty, r[i].y2) - col;
msk[i].reset();
tmp.reset();
tmp = (1 << (y2 - y1))- 1; //y1,y2是矩形边界,对于msk记录的矩形块来说 一行的块数是(y2 - y1)
tmp <<= (y1 - 1);
for(int j = x1 - 1; j < x2 - 1; j ++){
msk[i] |= (tmp << (j * (cnty - 1)));
}
// cout << msk[i] << endl;
}
//二进制枚举寻找最终态 (能完全覆盖矩形h,m的状态)
for(int i = 0; i < 1 << n; i ++){
tmp.reset();
for(int j = 0; j < n; j ++){
if(i >> j & 1) tmp |= msk[j + 1];
}
if(tmp.count() != s) f[i] = -1;
else f[i] = 0;
}
if(f[(1 << n) - 1] == -1) { // 特判
printf("-1\n");
return ;
}
ll p = inv[n];
//设i是当前状态, j为新增的矩形
for(int i = (1 << n) - 1; i >= 0; i --) {// 状压DP
if(!f[i]) continue ;
ll cnt = 0, res = 0;
for(int j = 0; j < n; j ++) { // 枚举染黑的点
if(i >> j & 1) cnt ++;// i中已经存在的状态
else res = (res + p * f[i | (1 << j)] % mod) % mod;
}
f[i] = (1 + res) * n % mod * inv[n - cnt] % mod;
}
printf("%lld\n", f[0]);
return ;
}
int main()
{
int T;
scanf("%d",&T);
inv[1] = 1;
for(int i = 2; i <= 20; i ++){
inv[i] = ksm(i, mod - 2);
}
while(T--)solve();
return 0;
}
关于逆推从已知最终态求解概率和期望的方式,曾在ABC遇到过一道经典的题,可以尝试练习:(AtCoder Beginner Contest 263)E - Sugoroku 3
本人题解:ABC 263 A~F题解
F-Find the Maximum
本场的签到题之一,但是由于被前几个概率题搞心态加上榜有点歪,还没来开到就下班没打了,下次还是需要多开题的。
题意
给定一颗 n n n个节点的数,每个节点的权值为 a i ai ai。定义树上路径的长度即为边数,求长度至少为 1 1 1的简单路径,以下公式的最大值: ∑ u ∈ V ( − x 2 + a u x ) ∣ V ∣ \frac{\sum_{u\in V}(-x^2+a_{u}x)}{|V|} ∣V∣∑u∈V(−x2+aux),其中 V V V为路径上点的集合, ∣ V ∣ |V| ∣V∣为集合的大小。
解析
观察式子,并将式子展开
设
∣
V
∣
=
m
|V| = m
∣V∣=m
y
=
(
−
m
x
2
+
∑
a
u
∗
x
)
/
m
y = (-mx ^ 2 + \sum au * x) / m
y=(−mx2+∑au∗x)/m
y
=
(
−
x
2
+
∑
a
u
∗
x
)
/
m
y = (-x ^ 2 + \sum au * x) / m
y=(−x2+∑au∗x)/m
发现式子是一个一元二次函数
当一元二次函数形如:
−
a
x
2
+
b
x
+
c
-ax^2 + bx + c
−ax2+bx+c时,最大值 在
x
=
b
−
2
a
x = \frac{b}{-2a}
x=−2ab时取到
设
p
=
∑
a
u
/
2
m
p = \sum au / 2m
p=∑au/2m,当
x
x
x取
p
p
p时
a
n
s
=
p
2
4
ans = \frac{p^2}{4}
ans=4p2。所以当
p
p
p越大时答案就越大。
我们先从简单的开始考虑 因为我们至少要取长度为
1
1
1的路径即至少两个点
p
=
(
a
[
u
]
+
a
[
v
]
)
/
(
2
∗
2
)
p = (a[u] + a[v]) / (2 * 2)
p=(a[u]+a[v])/(2∗2)
考虑多增加一个点
a
[
k
]
a[k]
a[k] 当
a
[
k
]
>
(
a
[
u
]
+
a
[
v
]
)
/
2
a[k] > (a[u] + a[v]) / 2
a[k]>(a[u]+a[v])/2时
p
p
p会增大 所以我们可以取长度为
2
2
2的路径
考虑再增加一个点
a
[
j
]
a[j]
a[j] 同理当
a
[
j
]
>
(
a
[
u
]
+
a
[
v
]
+
a
[
k
]
)
/
3
a[j] > (a[u] + a[v] + a[k]) / 3
a[j]>(a[u]+a[v]+a[k])/3时
p
p
p会增大
但是此时我们抛弃掉更小的两个只取
a
[
k
]
+
a
[
j
]
a[k] + a[j]
a[k]+a[j]是不是
p
p
p会更大
所以得出结论取的路径最小为
1
1
1,最大为
2
2
2,跑一遍
d
f
s
dfs
dfs即可
在求解的过程中会有三种情况
1.
1.
1.路径长度为
1
1
1,那么只取父节点和子节点。
2.
2.
2.路径长度为
2
2
2,那么取父节点和两个子节点。
3.
3.
3.路径长度为
2
2
2,那么取父节点,子节点和孙子节点。补题的时候少考虑了这一种情况wa了一发
第三种情况只需要找子节点中值最小的两个绝对值和最大的两个绝对值进行比较,这一步偷懒排了个序。
d
f
s
dfs
dfs时间复杂度
O
(
n
)
O(n)
O(n) 排序时间复杂度
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),最终复杂度
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)。
代码
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
vector<int>g[N];
int n,a[N],x,y;
void dfs(int u,int fa)
{
//少考虑一种情况,选3个点的情况时,可能是父节点和两个子节点
vector<int>tmp;
for(int v : g[u]){
if(v == fa) continue ;
tmp.push_back(a[v]);
if(2 * x < abs(a[u] + a[v]) * y){//父节点和子节点
x = abs(a[u] + a[v]);
y = 2;
}
if(fa && 3 * x < abs(a[fa] + a[u] + a[v]) * y){//父节点和子节点和孙子节点
x = abs(a[fa] + a[u] + a[v]);
y = 3;
}
dfs(v, u);
}
//父节点和两个子节点
sort(tmp.begin(), tmp.end());
int m = tmp.size();
if(m > 1){
if(3 * x < abs(tmp[0] + tmp[1] + a[u]) * y){
x = abs(a[u] + tmp[0] + tmp[1]);
y = 3;
}
if(3 * x < abs(tmp[m - 1] + tmp[m - 2] + a[u]) * y){
x = abs(a[u] + tmp[m - 1] + tmp[m - 2]);
y = 3;
}
}
}
int main()
{
scanf("%d",&n);
for(int i = 1; i <= n; i ++){
scanf("%d",&a[i]);
}
for(int i = 1; i < n; i ++){
int u,v;
scanf("%d%d",&u,&v);
g[u].push_back(v);
g[v].push_back(u);
}
x = 0;
y = 1;
dfs(1, 0);
double p = (double)x / (double)y;
double ans = p * p / 4;
printf("%.8lf",ans);
return 0;
}
E-Easy String Problem
题意
给定一个长度为 n n n的序列, q q q个询问,每次询问一个区间 [ l , r ] [l,r] [l,r],问任意删除一个包含该区间的子数组剩下的本质不同序列有多少种。当且仅当两个序列长度相同,且每一位都相同时认为两个序列相同。
解析
首先先从简单的问题开始考虑,假设只有一次询问
[
l
,
r
]
[l,r]
[l,r],且不考虑重复的序列,只询问有多少序列。那么答案显然为
a
n
s
=
l
∗
(
n
−
r
+
1
)
ans = l * (n - r + 1)
ans=l∗(n−r+1)(左端点可选的点的数量
∗
*
∗ 右)。
加上要去掉重复序列的条件,如何求有多少删除方式得到的序列是重复的呢。考虑这样这样一个序列
a
b
c
[
l
,
r
]
a
b
b
abc[l,r]abb
abc[l,r]abb
1.
1.
1.当删掉
[
l
[l
[l左边的bc时,序列变成
a
[
]
a
b
b
a[]abb
a[]abb 此时删除还剩下的两个
a
a
a的任意一个剩下的序列都是相同(1个重复的序列)。
2.
2.
2.当删掉
[
l
[l
[l左边的
c
c
c和
r
]
r]
r]右边的
a
a
a时,序列变成
a
b
[
]
b
b
ab[]bb
ab[]bb此时删除左边的
b
b
b和右边第一个
b
b
b任意一个剩下的序列都是相同的(1个重复序列),或者删除左右第一个
b
b
b等价于删除右边的两个
b
b
b。
所以重复的序列个数就是
[
l
,
r
]
[l,r]
[l,r]左右相同字母(数字)的的数量乘积之和。设
c
n
t
l
[
i
]
cntl[i]
cntl[i]为数
i
i
i在
[
l
,
r
]
[l,r]
[l,r]右边的个数,
c
n
t
l
[
i
]
cntl[i]
cntl[i]为数
i
i
i在
[
l
,
r
]
[l,r]
[l,r]左边的个数,对于一次询问有:
a
n
s
=
l
∗
(
n
−
r
+
1
)
−
∑
i
=
1
n
c
n
t
l
[
i
]
∗
c
n
t
r
[
i
]
ans = l * (n - r + 1) - \sum_{i=1}^{n}cntl[i]*cntr[i]
ans=l∗(n−r+1)−∑i=1ncntl[i]∗cntr[i]
q
q
q次询问每次都遍历数组求一遍
c
n
t
cnt
cnt时间复杂度达到了
O
(
n
∗
q
)
O(n*q)
O(n∗q),考虑如何优化,对于离线询问区间问题,我们想到莫队算法,每次移动左右指针时更新重复的序列数量。
考虑左指针左移一格,将上一次询问没有包括的
a
[
x
]
a[x]
a[x]包括进来,
a
[
x
]
a[x]
a[x]新加入一定要删除的区间,
a
[
x
]
a[x]
a[x]原本的贡献为
c
n
t
l
[
a
[
x
]
]
∗
c
n
t
r
[
a
[
x
]
]
cntl[a[x]] * cntr[a[x]]
cntl[a[x]]∗cntr[a[x]],次数
c
n
t
l
[
a
[
x
]
]
cntl[a[x]]
cntl[a[x]]减少
1
1
1,贡献减少为
(
c
n
t
l
[
a
[
x
]
]
−
1
)
∗
c
n
t
r
[
a
[
x
]
]
(cntl[a[x]] - 1) * cntr[a[x]]
(cntl[a[x]]−1)∗cntr[a[x]],那么计算重复序列时直接减去
c
n
t
r
[
a
[
x
]
]
cntr[a[x]]
cntr[a[x]]即可。指针的其他移动情况同理,具体实现在代码中。
代码
#include <math.h>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
#define ll long long
int len,a[N],cntl[N],cntr[N];
struct quer{
int l,r,id;
}q[N];
bool cmp(const quer &A, const quer &B){//奇偶分块优化
int id1 = A.l / len, id2 = B.l / len;
if(id1 != id2) return id1 < id2;
if(id1 & 1) return A.r < B.r;
else return A.r > B.r;
}
ll rep, ans[N];
void add(int x,int op){
if(!op){ //左指针左移
/*
a[x]新加入一定要删除的区间,a[x]原本的贡献为cntl[a[x]] * cntr[a[x]]
次数cntl[a[x]]减少1,贡献减少为(cntl[a[x]] - 1) * cntr[a[x]]。那么计算重复序列时直接减去cntr[a[x]]
*/
rep -= cntr[a[x]];
cntl[a[x]] --;
}
else{
rep -= cntl[a[x]];
cntr[a[x]] --;
}
}
void sub(int x,int op){
if(!op){//左指针右移
rep += cntr[a[x]];
cntl[a[x]] ++;
}
else{
rep += cntl[a[x]];
cntr[a[x]] ++;
}
}
int main()
{
int n,m;
scanf("%d",&n);
for(int i = 1; i <= n; i ++){
scanf("%d",&a[i]);
cntr[a[i]] ++;
}
scanf("%d",&m);
for(int i = 1; i <= m; i ++){
int l,r;
scanf("%d%d",&l,&r);
q[i] = {l, r, i};
}
len = sqrt(n);
len = max(len, 1);
sort(q + 1, q + 1 + m, cmp);
ll res = 0;
for(int i = 1, L = 1, R = 0; i <= m; i ++){
int l = q[i].l, r = q[i].r;
res = 1ll * l * (n - r + 1); // 每次计算序列总数
while(L < l) sub(L ++, 0);
while(L > l) add(-- L, 0);
while(R < r) add(++ R, 1);
while(R > r) sub(R --, 1);
ans[q[i].id] = res - rep;// rep 为每次移动指针后更新的重复序列数目,减去即为答案。
}
for(int i = 1; i <= m; i ++){
printf("%lld\n",ans[i]);
}
return 0;
}
更新:
2022.11.9 B
2022.11.10 F
2022.11.11 E
应该就更新到这里了,虽然还补了G但没太能理解,可以去看看其他大佬的博客。A大模拟写题解的意义不大,可以当阅读题和码力题练手。其他的题就超出我目前的水平了,暂时不补了。