解题报告 smoj 2019初二创新班(2019.3.24)
解题报告 smoj 2019初二创新班(2019.3.24)
时间:2019.3.29
T1:移动“哨兵棋子”
题目描述
给出数轴上的N个棋子,要求移动到连续的N个位置上,只能移动到整点,只能移动两端的“哨兵”棋子,且移动后不能是“哨兵”。求最小移动步数和最大移动步数。N <= 100000
分析
贪心,将“最小”和“最大”分开计算。
贪心策略见下。
最小移动步数
贪心策略:枚举移动之后得到的区间(必须在左右哨兵之间),计算这个区间最多包含多少棋子。若区间最多包含max_cnt
个棋子,那么答案就是n - max_cnt
。
证明:
![](./解题报告 smoj 2019初二创新班(2019.3.24)/1.png)
不妨假设区间的左端点一定是一个棋子,可以发现这是不会对答案产生影响的(超出右哨兵的区间可以看成以右哨兵为结尾)
直接使用双指针(牛吃草)扫描即可
注意这个贪心有一个问题:
![](./解题报告 smoj 2019初二创新班(2019.3.24)/2.png)
当有n - 1
个节点连在一起的时候,靠左的哨兵无法移动到剩余的一个空位。在程序里特判一下即可。
代码(最小移动步数)
cur = 1;
pos[n + 1] = kInf;
max_cnt = -1;
for (int i = 1; i <= n; i++) {
cur = max(cur, i);
while (pos[cur + 1] - pos[i] + 1 <= n) cur++;
if (cur - i + 1 == n - 1 && pos[cur] - pos[i] + 1 == n - 1) continue;
max_cnt = max(max_cnt, cur - i + 1);
}
assert(max_cnt != -1);
printf("%lld\n", n - max_cnt);
最大移动步数
贪心策略:让边缘的两个棋子交替前进,如图。
![](./解题报告 smoj 2019初二创新班(2019.3.24)/3.png)
每次哨兵移动,都会使空白框的长度减去1。讨论一下移动哪个哨兵即可
代码(最大移动步数)
这里choose_left
指左边空白的长度(即移动右哨兵),choose_right
同理。
for (int i = 1; i <= n; i++) {
if (i > 1 && i < n) choose_left += pos[i] - pos[i - 1] - 1;
if (i > 2) choose_right += pos[i] - pos[i - 1] - 1;
}
printf("%lld\n", max(choose_left, choose_right));
总代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int kMaxN = 100000 + 10;
const int kInf = 1000000000 + kMaxN + 10;
int n;
int pos[kMaxN];
int choose_left, choose_right;
int cur, max_cnt;
signed main() {
freopen("2819.in", "r", stdin);
freopen("2819.out", "w", stdout);
scanf("%lld", &n);
for (int i = 1; i <= n; i++) {
scanf("%lld", &pos[i]);
}
sort(pos + 1, pos + n + 1);
cur = 1;
pos[n + 1] = kInf;
max_cnt = -1;
for (int i = 1; i <= n; i++) {
cur = max(cur, i);
while (pos[cur + 1] - pos[i] + 1 <= n) cur++;
if (cur - i + 1 == n - 1 && pos[cur] - pos[i] + 1 == n - 1) continue;
max_cnt = max(max_cnt, cur - i + 1);
}
assert(max_cnt != -1);
printf("%lld\n", n - max_cnt);
for (int i = 1; i <= n; i++) {
if (i > 1 && i < n) choose_left += pos[i] - pos[i - 1] - 1;
if (i > 2) choose_right += pos[i] - pos[i - 1] - 1;
}
printf("%lld\n", max(choose_left, choose_right));
return 0;
}
T2:黑白球
题目描述
一个箱子里面有n个黑球m个白球。你每小时都随机从箱子里面抽出两个小球,然后把这两个球都染成黑球,然后再放回去。问需要多少小时才能把所有小球变成黑色小球?输出期望值。
分析
移项期望DP裸题。
设\(F(n, m)\)为剩下\(n\)个黑球,\(m\)个白球,将所有白球变成黑球的期望步数。
两次都取出黑球的概率:
两种颜色各取出一个的概率:
两次都取出白球的概率:
DP方程:
移项可得:
时间复杂度\(O(n)\),空间复杂度\(O(n^2)\)。
代码
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 47 * 4 + 10;
int T;
int n, m;
double arr_prob[kMaxN][kMaxN];
double GetProb(int n, int m) {
if (m <= 0) {
return 0;
} else if (arr_prob[n][m] >= -1) {
return arr_prob[n][m];
} else {
double invtot = 1.0 / (n + m);
double invtot2 = 1.0 / (n + m - 1);
double p1 = n * invtot * (n - 1) * invtot2;
double p2 = n * invtot * m * invtot2
+ m * invtot * n * invtot2;
double p3 = m * invtot * (m - 1) * invtot2;
return arr_prob[n][m] = (1 + GetProb(n + 1, m - 1) * p2
+ GetProb(n + 2, m - 2) * p3) / (1 - p1);
}
}
int main() {
freopen("2829.in", "r", stdin);
freopen("2829.out", "w", stdout);
scanf("%d", &T);
while (T--) {
scanf("%d %d", &n, &m);
for (int i = 0; i < kMaxN; i++)
for (int j = 0; j < kMaxN; j++)
arr_prob[i][j] = -5;
printf("%lf\n", GetProb(n, m));
}
return 0;
}
T3:树与图
题目描述
最近有人发明了一款新游戏。给定具有N个顶点的连通无向图和具有N个节点的树,尝试以下列方式将该树放置在图上:
1、树的每个节点与图的顶点对应。即每个节点对应一个顶点,每个顶点对应一个节点。
2、如果树的两个节点之间存在边,则图中的相应顶点之间也必须存在边。
现在想知道有多少不同的放置方案,答案模1000000007。
数据范围:N <= 14
分析
下文为了方便将“树上的点”简称“树点”,图同理。
有一个明显的暴力:枚举每个树点对应的图点。用全排列(Dfs)枚举即可。
让我们思考一下:为什么我们要枚举全排列而不是直接\(N^N\)选择?回答是因为树点要和图点一一对应,已被对应过的图点不能被其他树点再次对应了。
这让我们想到状压DP。引用@shadowice1984的话:
如果你足够熟练的话(参见ZJOI2015地震后的幻想乡&NOIP2017宝藏),你会发现全排列暴力的优化永远都是指向了一个东西——子集dp
状压DP的思路如下,由于笔者没有写过,所以不详细将方程列出:(仍然是引用,引用自@xyz32768)
定义状态\(f[i][j][S]\)表示节点\(i\)编号为\(j\),\(i\)的子树内的编号集合为\(S\)的方案数。
但是这样的瓶颈在于枚举子集,复杂度是\(O(n^3\times 3^n)\)的,显然TLE。
下面就让我们来看一种正确的解法
容斥 \(\times\) DP
DP
考虑DP。我们发现由于树中连边的限制,DP需要有一定的顺序(树上父子关系)。设\(F(u)\)为当前考虑树上以\(u\)为根的子树,并将其放在图上的方案数。图上的边也需要考虑,添加一维状态来添加限制。设\(\bf{F(u, map)}\)为当前考虑树上以\(\bf{u}\)为根的子树,并将其对应到图上的方案数。其中已经将\(\bf{u}\)与图点\(\bf{mapu}\)进行了对应。
这个方程没有考虑重复对应的问题。我们将在后面的容斥中将这个问题解决,因此方程维数可以不再增加了。
很容易得到转移方程,枚举\(u\)的子树\(v\),并枚举\(v\)所对应的图点即可。方程如下:
最终答案就是\(\displaystyle \sum _ {mapR} F(root, mapR)\)。使用记忆化搜索可以轻松实现,单次DP的时间复杂度是\(O(N^3)\)。
容斥
接下来是考虑重复对应的问题。根据DP的定义,我们发现计算得出的答案是会偏大的。这是因为在一些情况中,两个树点对应到一个图点上;在另一些情况中,三个树点对应到一个图点上……在这些情况中,有一些图点没有被对应。例如:若两个树点对应到一个图点,被对应的图点数就只有\(N - 1\)个。
不妨考虑枚举可能被对应到的图点的集合。由于DP方程的限制,集合中的图点也不是一定能被对应到,集合为\(\{1, 2, 3\}\)时的方案也可能包含\(\{1, 2\}\)。将加多了的减下去,减多了的加回去。直接上容斥就行了。
代码
另外一点,最终的答案不会超过long long
的限制,所以也可以输出时再取模。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int kMaxN = 15;
int T;
int n, global_set;
bool graph[kMaxN][kMaxN], tree[kMaxN][kMaxN];
LL arr_dp[kMaxN][kMaxN];
char str[kMaxN];
void ReadGraph(bool graph[kMaxN][kMaxN]) {
for (int i = 0; i < n; i++) {
scanf("%s", str);
for (int j = 0; j < n; j++) {
graph[i][j] = graph[j][i] = (str[j] == 'Y');
}
}
}
void Dfs(int u) { // 确定原树的根为0,将反向边删除
for (int v = 0; v < n; v++) {
if (tree[u][v]) {
tree[v][u] = false;
Dfs(v);
}
}
}
LL Dp(int u, int mapu) {
if (arr_dp[u][mapu] != -1) {
return arr_dp[u][mapu];
} else {
LL ans = 1;
for (int v = 0; v < n; v++) {
if (tree[u][v]) {
LL sum = 0;
for (int mapv = 0; mapv < n; mapv++) {
if (( global_set & (1 << mapv) ) && graph[mapu][mapv]) {
sum += Dp(v, mapv);
}
}
ans *= sum;
}
}
return arr_dp[u][mapu] = ans;
}
}
int main() {
freopen("2830.in", "r", stdin);
freopen("2830.out", "w", stdout);
scanf("%d", &T);
while (T--) {
memset(graph, false, sizeof(graph));
memset(tree, false, sizeof(tree));
scanf("%d", &n);
ReadGraph(graph);
ReadGraph(tree);
Dfs(0);
LL ans = 0;
for (global_set = 1; global_set < (1 << n); global_set++) {
memset(arr_dp, -1, sizeof(arr_dp));
LL sum = 0;
int cnt = 0;
for (int i = 0; i < n; i++) {
if (global_set & (1 << i)) {
sum += Dp(0, i);
cnt++;
}
}
if ((n - cnt) & 1) {
ans -= sum;
} else {
ans += sum;
}
}
printf("%lld\n", ans % 1000000007);
}
return 0;
}