上海计算机学会10月月赛 乙组题解
本次比赛主要涉及到两道二分答案,两道树形动态规划,需要思考和慢慢推导,普及组后期或提高组的小朋友可以做做,练习一下
比赛链接:https://iai.sh.cn/contest/54
第一题:T1保持距离
标签:二分答案
题意:给定
n
n
n个坐标
x
1
,
x
2
,
…
,
x
n
x_1,x_2,…,x_n
x1,x2,…,xn 请从中挑出
k
k
k个坐标(
k
k
k为给定值),使得选出的坐标两两之间的最短距离最大。
题解:非常经典的二分答案最大化最小值的题型,先对坐标从小到大排序,然后二分枚举最短距离
m
i
d
mid
mid,求当前情况下能够满足相邻坐标大于等于
m
i
d
mid
mid的有多少个,如果不够
k
k
k个,那么我们需要把这个距离变小一点(即把搜寻区间调整到
[
l
,
m
i
d
−
1
]
[l,mid-1]
[l,mid−1]);反之,够的话把距离变大(搜寻区间调整到
[
m
i
d
+
1
,
r
]
[mid+1,r]
[mid+1,r]),同时把
a
n
s
ans
ans更新一下。
tips:二分搜索的前提是序列是有序的,二分答案的前提是 满足条件的答案是单调有序的(具有单调性)
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll n, k, x[300005];
int main() {
cin >> n >> k;
for (int i = 1; i <= n; i++) cin >> x[i];
sort(x + 1, x + 1 + n);
ll l = 0, r = 1e10, ans = 1e10;
while (l <= r) {
ll mid = (l + r) >> 1;
// cnt: 选出的坐标个数 pre: 上一个的坐标
ll cnt = 1, pre = 1;
for (int i = 2; i <= n; i++) {
if (x[i] - x[pre] >= mid) {
cnt++;
pre = i;
}
}
if (cnt >= k) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
cout << ans << endl;
return 0;
}
第二题:T2社团展示
标签:贪心、思维、二分答案
题意:给定
n
n
n个社团,第
i
i
i个社团
x
i
x_i
xi名学生,需要去完成作品。每件作品得有至少
m
m
m个不同的社团成员合作完成,每个同学只能参与一个作品,求最多完成作品数量。
题解 1(部分正确):比较容易想到一个贪心策略:每次用人数最多的
m
m
m个社团去完成
m
i
n
{
a
i
}
min \{a_i \}
min{ai}个作品,可以通过优先队列去维护,每次拿出人数最多的
m
m
m个社团,能够形成的作品数目是当前拿出的
m
m
m个社团中最少人数那个社团,都减一下,然后扔回优先队列,不断模拟这个过程,直到优先队列中的社团个数不够
m
m
m个。
测了下,发现只有部分正确,思考一下这个策略问题出在哪?
可以看看以下这个样例:
6 2
8 8 9 10 1 4
序列:10 9 8 8 4 1
第一轮:10 9,答案9,序列变成:8 8 4 1 1
第二轮:8 8,答案 9 + 8,序列变成:4 1 1
第三轮:4 1,答案 9 + 8 + 1,序列变成:3 1
第四轮:3 1,答案 9 + 8 + 1 + 1,序列变成:2,最终答案为:19
但是实际上有更多完成数量的选择:
第一轮:8 9,答案 8,序列变成:10 8 4 1 1
第二轮:10 8,答案 8 + 8,序列变成:4 2 1 1
第三轮:4 2,答案 8 + 8 + 2,序列变成:2 1 1
第四轮:2 1,答案 8 + 8 + 2 + 1,序列变成:1 1
第五轮:1 1,答案 8 + 8 + 2 + 1 + 1,最终答案为 20
以上推理得到这个贪心策略是错误的。
代码 1:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
priority_queue<ll> q;
ll a[100005];
int main() {
ll n, m, x, ans = 0;
cin >> n >> m;
for (ll i = 1; i <= n; i++) {
cin >> x;
q.push(x);
}
while (q.size() >= m) {
for (ll i = 1; i <= m; i++) {
a[i] = q.top();
q.pop();
}
ans += a[m];
for (ll i = m; i >= 1; i--) {
if (a[i] - a[m] > 0) q.push(a[i] - a[m]);
}
}
cout << ans << endl;
return 0;
}
题解 2:这道题可以考虑直接二分答案(
m
i
d
mid
mid:作品数量),如果当前社团人数不少于作品数量
m
i
d
mid
mid,我们直接
c
n
t
+
1
cnt+1
cnt+1,每个作品这个社团都得出一个人;否则,直接
s
u
m
sum
sum把当前社团人数加起来,小于作品数量的社团一定存在不重叠的方案。本质来说就是挨个摞,摞完一个
m
i
d
mid
mid再摞下一堆。
最终判定一下
s
u
m
/
m
i
d
+
c
n
t
sum/mid+cnt
sum/mid+cnt和
m
m
m大小关系,对应调整作品数量的搜寻区间即可。
代码 2:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll x[100005];
int main() {
ll n, m, ans = 0;
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> x[i];
ll l = 0, r = 1e18;
while (l <= r) {
ll mid = (l + r) >> 1;
// cnt: 社团人数超过当前枚举作品数量的个数
// sum: 不超过当前枚举作品数量的人数
ll cnt = 0, sum = 0;
for (int i = 1; i <= n; i++) {
if (x[i] < mid) sum += x[i];
else cnt++;
}
if (sum / mid + cnt >= m) {
l = mid + 1;
ans = mid;
}
else {
r = mid - 1;
}
}
cout << ans;
return 0;
}
第三题:T3树的连通子图
标签:树、树形
d
p
dp
dp
题意:给定一棵
n
n
n个结点的树,
1
1
1号点为这棵树的根。计算这棵树连通子图的个数,答案对
1
,
000
,
000
,
007
1,000,000,007
1,000,000,007取余数。
题解:经典树形
d
p
dp
dp,对于每个节点
u
u
u分两种情况:
dp[u][0]:表示含有以u节点作为根的连通子图个数
dp[u][1]:表示不含有以u节点作为根的连通子图个数
- 对于节点 u u u的每个孩子节点 v v v来说,如果把 u u u算上去,连通子图的个数要做乘积(乘法原理)
- 对于节点 u u u的每个孩子节点 v v v来说,如果不把 u u u算上去,连通子图的个数要做相加(加法原理)
可以结合样例和给出的图,自己再推一推,想一想。做法是先把图存进邻接表里面,既然是根节点为
1
1
1,并且给出的都是每个节点的父亲节点,直接存单向边,然后从根节点
1
1
1从上往下遍历树,对应更新
d
p
[
u
]
[
0
]
dp[u][0]
dp[u][0]和
d
p
[
u
]
[
1
]
dp[u][1]
dp[u][1]即可。
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 1e9 + 7;
vector<ll> e[200005];
ll dp[200005][2];
// dp[u][0]: 含有以u节点作为根的连通子图个数
// dp[u][1]: 不含有以u节点作为根的连通子图个数
void dfs(ll u) {
dp[u][0] = 1;
for (int i = 0; i < e[u].size(); i++) {
ll v = e[u][i];
dfs(v);
dp[u][0] = (dp[u][0] * (dp[v][0] + 1)) % mod;
dp[u][1] = (dp[u][1] + dp[v][0] + dp[v][1]) % mod;
}
}
int main() {
ll n, x;
cin >> n;
for (int i = 2; i <= n; i++) {
cin >> x;
e[x].push_back(i);
}
dfs(1);
cout << (dp[1][0] + dp[1][1]) % mod << endl;
return 0;
}
第四题:T4树的覆盖
标签:树、最小点覆盖、树形
d
p
dp
dp
题意:求树的最小点覆盖集的大小和对应的数量,数量对
1
,
000
,
000
,
007
1,000,000,007
1,000,000,007取余数。
所谓覆盖集,是该树的点构成的集合,对树上每一条边,至少有一个顶点属于该集合。某个特定覆盖集的大小就是该集合中点的数量。
题解:
第一问:最小点覆盖集的大小比较好求,也是非常经典的一个模型,和上题树形
d
p
dp
dp差不多,对于每个节点
u
u
u分一下两种情况:
dp[u][0]:表示节点u属于点覆盖集,且以点u为根的子树中所连接的边都被覆盖的情况下点覆盖集中所包含最少点的个数。
dp[u][1]:表示点u不属于点覆盖集,并且以点u为根的子树中所连接的边都被覆盖的情况下点覆盖集中所包含最少点的个数。
以第二个样例举例:
4
4
4、
5
5
5、
6
6
6这三个节点很显然选和不选到覆盖集分别是
1
1
1和
0
0
0。
对于
2
2
2这个节点来说,如果选了,那么
2
2
2到
5
5
5这条边,可以直接覆盖,我们加上节点
5
5
5不选的情况(
d
p
[
5
]
[
1
]
dp[5][1]
dp[5][1]);如果不选,那么
2
2
2到
5
5
5这条边,得通过选节点
5
5
5(即加上
d
p
[
5
]
[
[
0
]
dp[5][[0]
dp[5][[0])。
对应节点
3
3
3同理,然后回到节点
1
1
1来看,我们可以把节点
1
1
1选了,那么节点
2
、
3
、
4
2、3、4
2、3、4选和不选无所谓了,反正节点
1
1
1能覆盖到和他们对应的边;不选节点
1
1
1,那么节点
2
、
3
、
4
2、3、4
2、3、4都得选上(即加上
d
p
[
2
]
[
0
]
+
d
p
[
3
]
[
0
]
+
d
p
[
4
]
[
0
]
dp[2][0]+dp[3][0]+dp[4][0]
dp[2][0]+dp[3][0]+dp[4][0])。
总结一下:
- 对于第一种状态 d p [ u ] [ 0 ] dp[u][0] dp[u][0],等于每个孩子节点的两种状态的最小值之和加 1 1 1,即 d p [ u ] [ 0 ] = 1 + Σ m i n ( d p [ v ] [ 0 ] , d p [ v ] [ 1 ] ) ( f a [ v ] = u ) dp[u][0]=1+Σmin(dp[v][0],dp[v][1]) \ (fa[v]=u) dp[u][0]=1+Σmin(dp[v][0],dp[v][1]) (fa[v]=u)
- 对于第二种状态 d p [ u ] [ 1 ] dp[u][1] dp[u][1],等于每个孩子节点的第一种状态之和,即 d p [ u ] [ 1 ] = Σ d p [ v ] [ 0 ] ( f a [ v ] = u ) dp[u][1]=Σdp[v][0] \ (fa[v]=u) dp[u][1]=Σdp[v][0] (fa[v]=u)
第二问:求最小覆盖集的数量,对于每个节点 u u u分一下两种情况:
cnt[u][0]:表示节点u属于点覆盖集,以点u为根的子树,当前最小覆盖集的数量
cnt[u][1]:表示点u不属于点覆盖集,以点u为根的子树,当前最小覆盖集的数量
- c n t [ u ] [ 1 ] cnt[u][1] cnt[u][1]的状态转移方程比较好想,它的所有孩子节点都得选进覆盖集,所以对所以孩子节点的 c n t [ v ] [ 0 ] cnt[v][0] cnt[v][0]做一个乘积。即 c n t [ u ] [ 1 ] = c n t [ u ] [ 1 ] ∗ c n t [ v ] [ 0 ] cnt[u][1] = cnt[u][1] * cnt[v][0] cnt[u][1]=cnt[u][1]∗cnt[v][0]
- c n t [ u ] [ 0 ] cnt[u][0] cnt[u][0]我们得去考虑,对于孩子节点 v v v来说是选还是不选能够获得更小的覆盖集大小,如果都能,得都加起来做一个乘积;否则从更小的情况转移过来做乘积。(详情看代码部分,可以自己再推导下)
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 1e9 + 7;
vector<ll> e[200005];
ll n, x, dp[200005][2], cnt[200005][2];
void dfs(ll u) {
dp[u][0] = 1;
dp[u][1] = 0;
cnt[u][0] = cnt[u][1] = 1;
for (ll i = 0; i < e[u].size(); i++) {
ll v = e[u][i];
dfs(v);
if (dp[v][0] == dp[v][1]) cnt[u][0] = cnt[u][0] * (cnt[v][0] + cnt[v][1]) % mod;
else if (dp[v][0] < dp[v][1]) cnt[u][0] = cnt[u][0] * cnt[v][0] % mod;
else if (dp[v][0] > dp[v][1]) cnt[u][0] = cnt[u][0] * cnt[v][1] % mod;
cnt[u][1] = cnt[u][1] * cnt[v][0] % mod;
dp[u][0] += min(dp[v][0], dp[v][1]);
dp[u][1] += dp[v][0];
}
}
int main() {
cin >> n;
for (int i = 2; i <= n; i++) {
cin >> x;
e[x].push_back(i);
}
dfs(1);
cout << min(dp[1][0], dp[1][1]) << endl;
if (dp[1][0] == dp[1][1]) cout << (cnt[1][0] + cnt[1][1]) % mod << endl;
else if (dp[1][0] < dp[1][1]) cout << cnt[1][0] % mod << endl;
else if (dp[1][0] > dp[1][1]) cout << cnt[1][1] % mod << endl;
return 0;
}