第一题:棋盘
在线测评链接:http://121.196.235.151/p/P1114
题目描述
薯条哥有一个 n × m n\times m n×m 的棋盘,一次移动可以选择上下左右四个方向移动一次,不同于普通棋盘,这个棋盘是循环的。
即 ( x , m ) (x, m) (x,m) 和 ( x , 1 ) (x, 1) (x,1) 两个点可以一步到达,其中 1 ≤ x ≤ n 1\leq x\leq n 1≤x≤n 。同样的, ( n , y ) (n, y) (n,y) 和 ( 1 , y ) (1, y) (1,y) 两个点也可以一步到达,其中 1 ≤ y ≤ m 1\leq y\leq m 1≤y≤m 。
现在薯条哥需要从 A A A 点先走到 B B B 点,再从 B B B 点走到 C C C 点,问最小移动次数是多少。
输入描述
第一行两个整数,
n
n
n 和
m
m
m 。
接下来三行,第一行是点
A
A
A 的坐标
(
x
A
,
y
A
)
(x_A,y_A)
(xA,yA),第二行是点
B
B
B 的坐标
(
x
B
,
y
B
)
(x_B,y_B)
(xB,yB) ,第三行是点
C
C
C 的坐标
(
x
C
,
y
C
)
(x_C,y_C)
(xC,yC)。
1 ≤ n , m ≤ 1 0 9 , 1 ≤ x A , x B , x C ≤ n , 1 ≤ y A , y B , y C ≤ m 1\leq n,m\leq 10^9, 1\leq x_A,x_B,x_C\leq n, 1\leq y_A,y_B,y_C\leq m 1≤n,m≤109,1≤xA,xB,xC≤n,1≤yA,yB,yC≤m
输出描述
输出从 A A A 到 B B B ,再从 B B B 到 C C C 的最小移动次数。
样例
输入
4 4
1 2
1 3
1 4
输出
2
题解:模拟
考虑任意点 ( a , b ) (a,b) (a,b)到 ( c , d ) (c,d) (c,d)的最小移动距离
首先,我们可以考虑从 a a a到 c c c的移动距离
- 方式1:直接到达: d i s t = a b s ( a − c ) dist=abs(a-c) dist=abs(a−c)
- 方式2:先走到 n n n,再由 n n n走到1,再由1走到 c c c, d i s t = n − a b s ( a − c ) dist=n-abs(a-c) dist=n−abs(a−c)
两种方式的移动取最小值, d i s t = m i n ( a b s ( a − c ) , n − a b s ( a − c ) ) dist=min(abs(a-c),n-abs(a-c)) dist=min(abs(a−c),n−abs(a−c))
因此 ( a , b ) (a,b) (a,b)到 ( c , d ) (c,d) (c,d)的最小移动距离 d i s t = m i n ( a b s ( a − c ) , n − a b s ( a − c ) ) + m i n ( a b s ( b − d ) , m − a b s ( b − d ) ) dist=min(abs(a-c),n-abs(a-c))+min(abs(b-d),m-abs(b-d)) dist=min(abs(a−c),n−abs(a−c))+min(abs(b−d),m−abs(b−d))
按照上述公式模拟即可
C++
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,m;
cin>>n>>m;
int xA,yA,xB,yB,xC,yC;
cin>>xA>>yA;
cin>>xB>>yB;
cin>>xC>>yC;
int res=0;
res+=min(abs(xA-xB),n-abs(xA-xB));
res+=min(abs(yA-yB),m-abs(yA-yB)); //计算A->B最短距离
res+=min(abs(xB-xC),n-abs(xB-xC));
res+=min(abs(yB-yC),m-abs(yB-yC)); // 计算B->C最短距离
cout<<res<<endl;
return 0;
}
Java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int m = scanner.nextInt();
int xA = scanner.nextInt();
int yA = scanner.nextInt();
int xB = scanner.nextInt();
int yB = scanner.nextInt();
int xC = scanner.nextInt();
int yC = scanner.nextInt();
int res = 0;
res += Math.min(Math.abs(xA - xB), n - Math.abs(xA - xB));
res += Math.min(Math.abs(yA - yB), m - Math.abs(yA - yB)); // 计算A到B的最短距离
res += Math.min(Math.abs(xB - xC), n - Math.abs(xB - xC));
res += Math.min(Math.abs(yB - yC), m - Math.abs(yB - yC)); // 计算B到C的最短距离
System.out.println(res);
}
}
Python
n, m = map(int, input().split())
xA, yA = map(int, input().split())
xB, yB = map(int, input().split())
xC, yC = map(int, input().split())
res = 0
res += min(abs(xA - xB), n - abs(xA - xB))
res += min(abs(yA - yB), m - abs(yA - yB)) # 计算A到B的最短距离
res += min(abs(xB - xC), n - abs(xB - xC))
res += min(abs(yB - yC), m - abs(yB - yC)) # 计算B到C的最短距离
print(res)
第二题:有根树的节点个数
在线评测链接:http://121.196.235.151/p/P1061
题目描述
薯条哥有一个 n n n 个节点的树,树根编号为 1 1 1 。
薯条哥可以在叶子节点上添加一个新的儿子节点,添加后,添加的节点变成了新的叶子节点。
若干次操作后,薯条哥想问你距离树根不超过 k k k 的节点最多可以有多少个。
输入描述
第一行,一个正整数 n n n 表示树中节点个数, k k k 表示不超过树根的距离,
接下来 n − 1 n-1 n−1 行,每行输入两个整数 u u u 和 v v v ,表示节点 u u u 和 v v v 之间有一条边。
1 ≤ n ≤ 1 0 5 , 1 ≤ k ≤ 1 0 9 , 1 ≤ u , v ≤ n 1 \leq n \leq 10^5, 1\leq k\leq 10^9, 1\leq u,v\leq n 1≤n≤105,1≤k≤109,1≤u,v≤n
输出描述
一个整数,表示若干次操作后距离树根不超过 k k k 的节点最大数量。
样例
输入
4 2
1 2
1 3
1 4
输出
7
说明
样例解释
本身有4个节点到根节点的距离不超过k(1,2,3,4)
叶子节点是(2,3,4) 还可以再添加一个节点
因此总共的数量为4+3=7
题解:树形DFS
本题需要用到贡献法计数的思想:就是考虑每一个节点对答案的贡献。
如果不进行任意的操作,那么对应的答案就是树中所有与根节点距离 ≤ k \le k ≤k的节点总数 c n t cnt cnt
我们现在需要去考虑:可以通过对叶子节点新增节点对答案的贡献。
首先,我们需要找到叶子结点,那什么样的节点是叶子结点?
显然,出度为0的节点就是叶子节点(入度为0的节点是根节点),但是本题给的是无向边,没办法去统计出度,我们换一个思路,如果一个节点是叶子结点,那么它有且仅有1条边与之相连(就是它的父节点和它构成的边)
知道了如何判断叶子节点,那么我们就需要考虑,对于每一个叶子节点,如何计算其对答案的贡献,也就是需要考虑其可以添加几个节点。这其实就跟叶子结点距离根节点的距离有关。
我们定义 d i s t [ i ] dist[i] dist[i]表示节点 i i i到根节点的距离,初始化 d i s t [ 1 ] = 0 dist[1]=0 dist[1]=0
那么,在我们进行DFS遍历的时候,如果 u u u是 v v v的子节点,一定有 d i s t [ u ] = d i s t [ v ] + 1 dist[u]=dist[v]+1 dist[u]=dist[v]+1
因此,我们按照上一道题:子树节点个数的dfs
代码,可以预处理出
d
i
s
t
dist
dist数组的值。
然后,对于每一个叶子结点,我们可以把答案累加上 a n s = a n s + m a x ( 0 , k − d i s t [ i ] ) ans=ans+max(0,k-dist[i]) ans=ans+max(0,k−dist[i])
注意:本题数据范围较大,
a
n
s
ans
ans变量需要取long long
C++
#include <bits/stdc++.h>
using namespace std;
const int N=1E5+10;
vector<int>g[N]; //领接表的vector写法 仅适用于点权建图
int dist[N]; //dist[i]记录节点i到根节点的距离
long long ans;
int n,k;
void dfs(int u,int fa) //如果是有向图 就不需要fa这个变量
{
if(dist[u]<=k){
ans++;
}
if(g[u].size()==1&&u!=1){ //u节点的度为1,且u节点不为根节点,说明该节点为叶子节点
ans+=max(0,k-dist[u]); //方案数累加
}
for(int &v:g[u]){
if(v==fa)continue;
dist[v]=dist[u]+1;
dfs(v,u);
}
}
int main(){
cin>>n>>k;
for(int i=1;i<n;i++){
int a,b;
cin>>a>>b;
g[a].push_back(b); //a->b建立一条边
g[b].push_back(a); //b->a建立一条边
}
dist[1]=0; //初始化根节点的值
dfs(1,0); //从根节点开始,自顶向下搜索
cout<<ans<<endl;
return 0;
}
Java
import java.util.*;
public class Main {
static final int N = (int)1e5+10;
static List<Integer>[] g = new ArrayList[N]; // 邻接表的ArrayList写法,仅适用于点权建图
static int[] dist = new int[N]; // dist[i]记录节点i到根节点的距离
static long ans;
static int n, k;
static void dfs(int u, int fa) { // 如果是有向图,就不需要fa这个变量
if(dist[u] <= k) {
ans++;
}
if(g[u].size() == 1 && u != 1) { // u节点的度为1,且u节点不为根节点,说明该节点为叶子节点
ans += Math.max(0, k - dist[u]); // 方案数累加
}
for(int v : g[u]) {
if(v == fa) continue;
dist[v] = dist[u] + 1;
dfs(v, u);
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
k = sc.nextInt();
for(int i = 1; i <= n; i++) {
g[i] = new ArrayList<>();
}
for(int i = 1; i < n; i++) {
int a = sc.nextInt();
int b = sc.nextInt();
g[a].add(b); // a->b建立一条边
g[b].add(a); // b->a建立一条边
}
dist[1] = 0; // 初始化根节点的值
dfs(1, 0); // 从根节点开始,自顶向下搜索
System.out.println(ans);
}
}
Python3
import sys
sys.setrecursionlimit(10**6) # 设置递归深度上限
from collections import defaultdict
N = int(1e5) + 10
g = defaultdict(list) # 邻接表的字典写法,仅适用于点权建图
dist = [0] * N # dist[i]记录节点i到根节点的距离
ans = 0
n, k = 0, 0
def dfs(u, fa): # 如果是有向图,就不需要fa这个变量
global ans
if dist[u] <= k:
ans += 1
if len(g[u]) == 1 and u != 1: # u节点的度为1,且u节点不为根节点,说明该节点为叶子节点
ans += max(0, k - dist[u]) # 方案数累加
for v in g[u]:
if v == fa:
continue
dist[v] = dist[u] + 1
dfs(v, u)
n, k = map(int, input().split())
for _ in range(1, n):
a, b = map(int, input().split())
g[a].append(b) # a->b建立一条边
g[b].append(a) # b->a建立一条边
dist[1] = 0 # 初始化根节点的值
dfs(1, 0) # 从根节点开始,自顶向下搜索
print(ans)
第三题:抽卡程序
在线测评链接:http://121.196.235.151/p/P1115
题目描述
薯条哥很喜欢抽卡的感觉,但是他又没有那么多钱给游戏充钱,所以他写了个抽卡程序。
普通抽卡过程有三种抽卡结果,5星常驻卡、5星限定卡和非5星卡。
抽到5星常驻卡和5星限定卡的概率均为 p 2 \frac{p}{2} 2p ,抽到非5星卡的概率为 1 − p 1-p 1−p 。
如果抽到了5星常驻卡,则之后的抽卡结果将发生变化,变为
p
p
p 的概率抽到5星限定卡,
1
−
p
1-p
1−p 的概率抽到非5星卡。
如果连续89次都未抽到5星卡,则第90抽必然会抽到一张5星常驻卡或5星限定卡。
现在薯条哥想问你,他写的这个抽卡程序抽到5星限定卡的抽卡次数期望是多少?
输入描述
一个小数 p p p 表示抽卡概率, 0 < p < 1 0 < p < 1 0<p<1
输出描述
一个小数,表示抽到5星限定卡的抽卡次数期望,请你将输出结果保留小数点后7位。
样例
输入
0.001
输出
129.1649522
题解:树形DP
本题有一个非常巧妙的做法, 但考虑到让大家更好地理解期望DP,我还是从一个最基本的DP的思想来跟大家讲解这道题的思路。
首先,显然如果薯条哥非常非,他需要抽满180次,才能保证自己抽到5星限定卡,也就是说,我们如果定义 f [ i ] f[i] f[i]为第 i i i次抽到5星限定卡的概率,那么总的期望就为 E = ∑ i = 1 180 f [ i ] × i E=\sum_{i=1}^{180}f[i]\times i E=∑i=1180f[i]×i
我们先考虑一个简单的情况,也就是前90抽的情况
我们发现一个很重要的结论:5星常驻卡和5星限定卡的出货概率是独立且相同的,各占50%!
我们根据动态规划的三要素:状态方程定义、状态初始化、状态转移方程来分析这道题
状态方程定义
为了便于我们后面计算,我们定义 f [ i ] f[i] f[i]为抽卡次数为 i i i抽到5星卡的概率。
状态初始化
所有状态初始化为0即可。
状态转移方程
我们发现,第 i i i次抽到5星卡的概率,说明前 i − 1 i-1 i−1次都没有抽到5星卡,并且第 i i i次抽到了5星卡
因此有 f [ i ] = ( 1 − ∑ j = 1 j = i − 1 f [ j ] ) × p f[i]=(1-\sum_{j=1}^{j=i-1}f[j])\times p f[i]=(1−∑j=1j=i−1f[j])×p
然后,对于第 i i i次抽到5星限定卡的期望就等于 f [ i ] × i × 1 2 f[i]\times i\times \frac{1}{2} f[i]×i×21
特殊情况: i = 90 i=90 i=90的时候, p p p为固定值0.5。
然后我们考虑从91~180抽的期望的计算方式
我们可以发现,这个是有非常多的情况,我们拿81抽出货举例,有以下几种情况
- 情况1:第1抽就出了5星卡,但是歪到了常驻5星,然后又接连抽了90抽,才抽到限定5星
- 情况2:第2抽就出了5星卡,但是歪到了常驻5星,然后又接连抽了89抽,才抽到限定5星
- 情况3:第3抽就出了5星卡,但是歪到了常驻5星,然后又接连抽了88抽,才抽到限定5星
因此,我们可以写一个双重循环,来枚举这些情况,注意,歪了常驻5星之后,后面每次抽到限定5星的概率不需要除以2(大保底必定不歪!!)
具体可以参考下面代码和代码中的注释
C++
#include <bits/stdc++.h>
using namespace std;
double f[91]; //f[i]表示抽卡次数为i的时候,出五星卡的概率
double s[91]; //s[i]表示f[i]的前缀和数组
double p; //出五星的概率
int main() {
double res=0;
cin>>p;
for(int i=1;i<90;i++){ //先计算1~89抽的期望,并更新f[i]和s[i]数组
f[i]=(1-s[i-1])*p;
res+=f[i]*i/2.0; //累加期望(出限定五星的概率仅为50%)
s[i]=s[i-1]+f[i]; //前缀和数组更新
}
f[90]=(1-s[89])*1.0; //第90抽必出五星
res+=90*f[90]/2.0; //第90抽的期望,出限定五星的概率仅为50%
for(int i=1;i<=90;i++){ //考虑小保底之后出货的期望
for(int j=1;j<=90;j++){
double p1=f[i]/2.0,p2=f[j]; //p1为第i抽抽到常驻五星的概率,p2为第i抽之后,抽到第j抽抽到限定五星的概率
res+=(i+j)*p1*p2;
}
}
cout << fixed;
cout.precision(7); //保留小数点后7位
cout <<res<<endl;
return 0;
}
Java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
double[] f = new double[91]; // f[i]表示抽卡次数为i的时候,出五星卡的概率
double[] s = new double[91]; // s[i]表示f[i]的前缀和数组
double p; // 出五星的概率
Scanner scanner = new Scanner(System.in);
p = scanner.nextDouble();
double res = 0;
for (int i = 1; i < 90; i++) { // 先计算1~89抽的期望,并更新f[i]和s[i]数组
f[i] = (1 - s[i - 1]) * p;
res += f[i] * i / 2.0; // 累加期望(出限定五星的概率仅为50%)
s[i] = s[i - 1] + f[i]; // 前缀和数组更新
}
f[90] = (1 - s[89]) * 1.0; // 第90抽必出五星
res += 90 * f[90] / 2.0; // 第90抽的期望,出限定五星的概率仅为50%
for (int i = 1; i <= 90; i++) { // 考虑小保底之后出货的期望
for (int j = 1; j <= 90; j++) {
double p1 = f[i] / 2.0, p2 = f[j]; // p1为第i抽抽到常驻五星的概率,p2为第i抽之后,抽到第j抽抽到限定五星的概率
res += (i + j) * p1 * p2;
}
}
System.out.printf("%.7f\n", res); // 保留小数点后7位
}
}
Python
f = [0.0] * 91 # f[i]表示抽卡次数为i的时候,出五星卡的概率
s = [0.0] * 91 # s[i]表示f[i]的前缀和数组
p = float(input()) # 出五星的概率
res = 0.0
for i in range(1, 90): # 先计算1~89抽的期望,并更新f[i]和s[i]数组
f[i] = (1 - s[i - 1]) * p
res += f[i] * i / 2.0 # 累加期望(出限定五星的概率仅为50%)
s[i] = s[i - 1] + f[i] # 前缀和数组更新
f[90] = (1 - s[89]) * 1.0 # 第90抽必出五星
res += 90 * f[90] / 2.0 # 第90抽的期望,出限定五星的概率仅为50%
for i in range(1, 91): # 考虑小保底之后出货的期望
for j in range(1, 91):
p1 = f[i] / 2.0 # p1为第i抽抽到常驻五星的概率
p2 = f[j] # p2为第i抽之后,抽到第j抽抽到限定五星的概率
res += (i + j) * p1 * p2
print("{:.7f}".format(res)) # 保留小数点后7位