2020 Multi-University Training Contest 5(2020杭电多校训练赛第五场)
| 题号 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | 1013 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 赛中 | 🎈 | 🎈 | 🎈 | 🎈 | 🎈 | ||||||||
| 赛后 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
1001 - Tetrahedron
题意
整数\(a,b,c\)相互独立地在\([1,n]\)内等概率取值,构成直角四面体的三条直角边,设\(h\)为直角顶点到斜面的欧几里得距离,求\(\frac{1}{h^2}\)的期望值。
分析
直角四面体体积\(V=\frac{1}{6}abc\),对于四面体又有\(V=\frac{1}{3}S_{斜面}h\),于是可以计算\(h\)。
赛中猜了一下结果是\(a,b,c\)一直取相同值时算得\(\frac{1}{h^2}\)的平均值,然后就过了...
依据题解是要套用海伦公式或向量叉积推得结果:\(\frac{1}{h^2}=\frac{1}{a^2}+\frac{1}{b^2}+\frac{1}{c^2}\),由于\(a,b,c\)相互独立,则有\(E(\frac{1}{h^2})=3E(\frac{1}{a^2})\),因此赛中把\(a,b,c\)当成一直取相同值来算是可以得到正确结果的。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int MOD = 998244353;
const int maxn = 6e6 + 10;
LL qpow(LL a, LL b)
{
LL res = 1;
a %= MOD;
while(b)
{
if(b & 1)
res = res * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return res;
}
int n;
LL res[maxn];
void preprocess()
{
LL ans = 0;
for(int i = 1; i < maxn; i++)
{
ans = (ans + 3 * qpow((long long)i * i % MOD, MOD - 2) % MOD) % MOD;
res[i] = ans * qpow(i, MOD - 2) % MOD;
}
}
int main()
{
preprocess();
int T;
scanf("%d", &T);
while(T--)
{
scanf("%d", &n);
printf("%lld\n", res[n]);
}
return 0;
}
1002 - Funny String
题意
分析
代码
1003 - Boring Game
题意
将一摞纸(\(n\)张)从左向右折叠\(k\)次,然后从上向下对每一面编号,要求输出依次最后每张纸上每一面的编号。(\(1\le n\le 200,1\le k\le 10,\sum 2\times n\times 2^k\le 10^6\))
分析
用栈模拟过程即可,一开始,将一摞未折叠的纸每张每面的要填标号的的\(2\times n\times 2^k\)个位置从上向下依次压入对应的\(2^k\)个栈中,每次折叠,将左半边的栈中内容弹出压入右半边的对应栈中,\(k\)次后只剩下一个栈,依次弹出将编号赋给对应位置。
时间复杂度\(O(nk2^k)\)
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int MOD = 998244353;
const int maxn = 1e6 + 10;
int n, k, m;
int t[20], a[maxn], b[maxn];
void preprocess()
{
t[0] = 1;
for(int i = 1; i <= 10; i++)
t[i] = t[i-1] * 2;
}
stack<int> s[2000];
int main()
{
preprocess();
int T;
scanf("%d", &T);
while(T--)
{
scanf("%d %d", &n, &k);
m = 2 * n * t[k];
for(int i = 1; i <= m; i++)
scanf("%d", &a[i]);
for(int i = 2 * n - 1; i >= 0; i--)
for(int j = 0; j < t[k]; j++)
s[j+1].push(i * t[k] + j);
for(int i = 1, mid; i < t[k]; i = mid + 1)
{
mid = (i + t[k]) / 2;
for(int j = 1; j <= mid - i + 1; j++)
{
while(!s[i+j-1].empty())
{
s[t[k]-j+1].push(s[i+j-1].top());
s[i+j-1].pop();
}
}
}
int cur = 0;
while(!s[t[k]].empty())
{
b[s[t[k]].top()] = a[++cur];
s[t[k]].pop();
}
for(int i = 0; i < m; i++)
{
if(i > 0)
printf(" ");
printf("%d", b[i]);
}
printf("\n");
}
return 0;
}
1004 - Expression
题意
分析
代码
1005 - Array Repairing
题意
分析
代码d
1006 - Alice and Bob
题意
分析
代码
1007 - Tree
题意
一棵带边权树,要从中选出一个连通子图,至多只能有一个节点的度大于 k,要求选出的连通子图边权和最大。
分析
先转为有根树,一遍 dp 出每个节点选 k - 1 个儿子的最大答案,这个显然要贪心,将儿子的答案排序选前 k - 1 个即可。枚举一个点,它能选全部的儿子,这显然可以换根,并在换根的时候统计该点作为根的答案,不用 set 是不会卡常的,换根的时候要重新维护 dp。
比赛的时候因为没有特判 k = 0 的情况,以及每个点选 k - 1 个儿子写成了选 k 个,硬调了1个小时。
代码
#include<bits/stdc++.h>
using namespace std;
#define pii pair<int,int>
#define fir first
#define sec second
typedef long long ll;
const int maxn = 2e5 + 10;
struct node {
int v;
ll val;
node() {}
node(int vi,ll tv) {
v = vi;
val = tv;
}
bool operator < (const node &rhs) const {
if (val == rhs.val) return v < rhs.v;
return val > rhs.val;
}
};
ll dp[maxn], ans, res[maxn];
vector<node> h[maxn];
vector<pii> g[maxn];
int q, n, k, vis[maxn];
void dfs1(int u,int fa) {
dp[u] = 0;
for (auto it : g[u]) {
if (it.fir == fa) continue;
dfs1(it.fir,u);
h[u].push_back(node(it.fir,it.sec + dp[it.fir]));
}
sort(h[u].begin(), h[u].end());
for (int i = 0; i < h[u].size() && i < k - 1; i++)
dp[u] = dp[u] + h[u][i].val;
}
void dfs2(int u,int fa) {
vis[u] = res[u] = dp[u] = 0;
int tot = h[u].size(), sz = 0;
sort(h[u].begin(),h[u].end());
for (int i = 0; i < h[u].size() && i < k - 1; i++) {
vis[h[u][i].v] = 1;
dp[u] = dp[u] + h[u][i].val;
}
for (auto it : h[u])
res[u] += it.val;
ans = max(ans,res[u]);
for (auto it : g[u]) {
if (it.fir == fa) continue;
if (tot < k) {
ll tv = dp[u] - dp[it.fir] - it.sec;
h[it.fir].push_back(node(u,it.sec + tv));
} else {
ll tv = dp[u];
if (vis[it.fir] == 1) {
tv -= (dp[it.fir] + it.sec);
tv += h[u][k - 1].val;
}
h[it.fir].push_back(node(u,it.sec + tv));
}
dfs2(it.fir,u);
}
}
int main() {
scanf("%d",&q);
while (q--) {
scanf("%d%d",&n,&k);
ans = 0;
for (int i = 1; i <= n; i++)
vis[i] = res[i] = 0, g[i].clear(), h[i].clear();
for (int i = 1; i < n; i++) {
int u, v, w; scanf("%d%d%d",&u,&v,&w);
g[u].push_back(pii(v,w));
g[v].push_back(pii(u,w));
}
if (k == 0) {
puts("0"); continue;
}
dfs1(1,0); dfs2(1,0);
printf("%lld\n",ans);
}
return 0;
}
1008 - Set2
题意
set1 的 hard 版本,每次可以删 k 个数字。
分析
这题感觉思维细节比较多,要一步一步理清思路。
首先最后只会剩下 \(n \% (k + 1)\) 个数字,令 \(r = n \% (k + 1)\),如果 \(r = 0\) ,则所有数字留下的概率都为 0,如果 \(n \leq k\),所有数字留下的概率都为 \(1\)。
考虑这 \(r\) 个数字的最小值,如果这 \(r\) 个数字的最小值是 \(i\),那么前 \(i - 1\) 个数字一定要删除,后 \(n - i\) 个数字种还要删除 \(n - i - r + 1\) 个,并且只能通过第二种操作来删除(如果后面还有数字是被第一种操作删除的,那么 \(i\) 是不可能留下的)。
考虑如何求得 前 \(i - 1\) 个数字被删除,第二种操作 还剩 \(n - i - r + 1\) 个的方案数:
令 \(dp[i][j]\) 表示前 \(i\) 个数字均被删除,还剩 \(j\) 次第二种操作的方案数,转移通过枚举第 i 个数字是被第一种操作删除还是第二种操作删除,有:
若 \(i + 1\) 被第一种操作删除,则 \(dp[i + 1][j + k] += dp[i][j]\)
若 \(i + 1\) 被第二种操作删除,则 \(dp[i + 1][j - 1] += j * dp[i][j]\)
对于每个数字 \(x\) ,枚举最后留下的集合中的最小值 \(i\):
若 \(i < x\), \(x\) 留下有 \(dp[i][n - i - r + 1] * C(n - i - 1,n - i - r + 1) * (n - i - r + 1)!\) 种方案
若 \(x = i\), \(x\) 留下有 \(dp[i][n - i - r + 1] * C(n - i,n - i - r + 1) * (n - i - r + 1)!\) 种方案
将每个值留下的方案数加起来作为总方案数,每个值的方案数除总方案数就得到每个值留下的概率
另一种思路借鉴于 njust "跃迁引擎" 的一位 dalao:

代码
#include<bits/stdc++.h>
using namespace std;
const int mod = 998244353;
const int maxn = 5e3 + 5;
typedef long long ll;
int t, n, k;
ll dp[maxn][maxn], cnt[maxn], sum[maxn], f[maxn], fac[maxn], comb[maxn][maxn], ans[maxn];
//dp[i][j] 表示前 i 个已经被删除,第二种操作还要做 j 次的方案数
ll fpow(ll a,ll b) {
ll r = 1;
while (b) {
if (b & 1) r = 1ll * r * a % mod;
b >>= 1;
a = 1ll * a * a % mod;
}
return r;
}
int main() {
fac[0] = 1;
for (int i = 1; i <= 5000; i++)
fac[i] = fac[i - 1] * i % mod;
comb[0][0] = 1;
for (int i = 1; i <= 5000; i++) {
comb[i][0] = 1;
for (int j = 1; j <= i; j++)
comb[i][j] = (comb[i - 1][j] + comb[i - 1][j - 1]) % mod;
}
scanf("%d",&t);
while (t--) {
scanf("%d%d",&n,&k);
int r = n % (k + 1);
dp[0][0] = 1;
for (int i = 1; i <= n; i++)
for (int j = 0; j <= n; j++)
dp[i][j] = 0;
for (int i = 0; i <= n; i++)
ans[i] = f[i] = sum[i] = cnt[i] = 0;
if (r == 0) {
for (int i = 1; i <= n; i++)
printf("0%s",i == n ? "\n" : " ");
continue;
} else if (n <= k) {
for (int i = 1; i <= n; i++)
printf("1%s",i == n ? "\n" : " ");
continue;
} else {
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= n; j++) {
if (j + k <= n - i - 1)
dp[i + 1][j + k] = (dp[i + 1][j + k] + dp[i][j]) % mod;
if (j > 0)
dp[i + 1][j - 1] = (dp[i + 1][j - 1] + 1ll * j * dp[i][j] % mod) % mod;
}
}
for (int i = 1; i <= n; i++) {
if (n - i + 1 >= r)
f[i] = 1ll * dp[i - 1][n - i - r + 1] * comb[n - i][n - i - r + 1] % mod * fac[n - i - r + 1] % mod;
else
f[i] = 0;
}
for (int i = 1; i < n; i++) {
if (n - i + 1 >= r) {
cnt[i] = 1ll * dp[i - 1][n - i - r + 1] * comb[n - i - 1][n - i - r + 1] % mod * fac[n - i - r + 1] % mod;
} else {
cnt[i] = 0;
}
sum[i] = (sum[i - 1] + cnt[i]) % mod;
}
ll Sum = 0;
for (int i = 1; i <= n; i++)
ans[i] = (f[i] + sum[i - 1]) % mod, Sum = (Sum + f[i]) % mod;
ll inv = fpow(Sum,mod - 2);
for (int i = 1; i <= n; i++)
printf("%lld%s",1ll * ans[i] * inv % mod,i == n ? "\n" : " ");
}
}
return 0;
}
1009 - Paperfolding
题意
对一张纸,随机产生 n 个操作,操作有四种类型:从左往右折,从右往左折,从上往下折,从下往上折,对最后的纸横竖各切一刀,求纸的期望数量。
分析
拿个纸折一下会发现,横向折会影响竖刀口的数量,竖着折会影响横刀口的数量,若横刀口为 x,竖刀口为 y,纸显然为 \((2^{x} + 1) * (2^y + 1)\) 张,再观察一下可以发现折纸的顺序,不影响对刀口数量的影响。
所有操作的可能数为 \(4^n\),考虑枚举 n 个操作有几个横折操作,设有 \(x\) 个,这种方案的概率为:\(\frac{C(n,x)*2^x*2^{n-x}}{4^n}\),贡献为 \((2^{x} + 1) * (2^{n-x} + 1)\),拆开得到 \(2^n + 1 + 2^x + 2^{n-x}\),后面两项求和就是 \(2*3^n\),因此答案就是 \(2^n + 1 + \frac{2 * 3^n}{2^n}\)
代码
#include<bits/stdc++.h>
using namespace std;
const int mod = 998244353;
const int maxn = 5e5 + 10;
typedef long long ll;
int t;
ll fpow(ll a,ll b) {
ll r = 1;
while (b) {
if (b & 1) r = r * a % mod;
b >>= 1;
a = a * a % mod;
}
return r;
}
int main() {
scanf("%d",&t);
while (t--) {
ll n;
scanf("%lld",&n);
ll t1 = 2 * fpow(3,n) % mod, t2 = fpow(2,n);
ll res = t1 * fpow(t2,mod - 2) % mod;
printf("%lld\n",(res + t2 + 1) % mod);
}
return 0;
}
1010 - Function
题意
分析
代码
1011 - Exam
题意
分析
代码
1012 - Set1
题意
有一个集合有 n 个数字 {1...n},n为奇数,每次删掉最小值,然后再随机删掉一个值,求每个数字最后留下的概率。
分析
观察一下发现小于 \(\frac{n}{2}\) 的数字一定无法留下,而大于 \(\frac{n}{2}\) 的数字要想留下,那么它的左边的数作为最小值时,随机删的数要带上它右边的数,若左边的数比右边的多,剩下的数两两匹配即可。
具体来说,对于数字 i,比它小的数字有 \(i - 1\) 个,比它大的数字有 \(n - i\) 个,若 \(i - 1 < n - i\) ,i 是无法留下的,反之要从 \(i - 1\) 个数中选出 \(n - i\) 个数和比它大的 \(n - i\) 个数进行匹配,匹配的方案数为 \((n - i)!\),对于左边剩下的数,可以两两任意匹配,这个可以通过 dp 来预处理,由于剩下的数一定是偶数,只需要处理偶数情况,通过枚举哪个数和第 n 个数匹配进行转移,转移方程为:\(dp[n] = (n - 2) * dp[n - 2]\)。由于删数的过程是每次先删最小值,匹配方案一经确定,那么删数的序列是确定的。
那么可以得出 \(i\) 要留下有 \(C(i - 1,n - i) * (n - i)! * dp[i - 1 - n + i]\) 种方案,把所有的方案加起来,就可以求出每个数留下的概率。
代码
#include<bits/stdc++.h>
using namespace std;
const int mod = 998244353;
const int maxn = 5e6 + 10;
const int N = 5e6;
typedef long long ll;
ll fac[maxn], ifac[maxn], dp[maxn], ans[maxn];
int t, n;
ll fpow(ll a,ll b) {
ll r = 1;
while (b) {
if (b & 1) r = r * a % mod;
b >>= 1;
a = a * a % mod;
}
return r;
}
ll C(int x,int y) {
if (x < y || y < 0) return 0;
return fac[x] * ifac[y] % mod * ifac[x - y] % mod;
}
int main() {
fac[0] = 1;
for (int i = 1; i <= N; i++)
fac[i] = fac[i - 1] * i % mod;
ifac[N] = fpow(fac[N],mod - 2);
for (int i = N - 1; i >= 0; i--)
ifac[i] = ifac[i + 1] * (i + 1) % mod;
dp[0] = 1;
for (int i = 2; i <= N; i += 2) {
dp[i] = (i - 1) * dp[i - 2] % mod;
}
scanf("%d",&t);
while (t--) {
ll n, sum = 0;
scanf("%d",&n);
for (int i = 1; i <= n; i++) {
ans[i] = C(i - 1,n - i) * fac[n - i] % mod * dp[i - 1 - n + i] % mod;
sum = (sum + ans[i]) % mod;
}
ll inv = fpow(sum,mod - 2);
for (int i = 1; i <= n; i++) {
//printf("%lld %lld\n",i,ans[i]);
printf("%lld%s",ans[i] * inv % mod,i == n ? "\n" : " ");
}
}
return 0;
}

浙公网安备 33010602011771号