暑假集训D10 2023.8.3 补题
题目来源 \(GCPC2023\)
D.DnD Dice
给出分别有不同个数的 \(4,6,8,12,20\) 面骰子, \(k\) 面骰子的每个面的点数分别是 \(1\sim k\) .
问用上所有骰子能组合出来的情况的概率从大到小排序,如果有相同的可能性的情况,按任意顺序即可.
\(\operatorname{Solution 1}\)
可以将骰子两两合并,合并后的骰子大小为 \([min_a+min_b,max_a+max_b]\) , 对合成后的新骰子点数一定 \(\in [min_a+min_b,max_a+max_b]\) ,并且有 $f[i+j] = f[i] \times f[j] $. 其中 \(f[i]\) 表示某个骰子掷到 \(i\) 的概率(特别地,对于一个没有合成过的骰子,概率是 \(1\div n\),其中 \(n\) 为该骰子的面数).由于最后合成一个大骰子,某一点数 \(i\) 概率一定是所有可能的情况总数之和除以所有合并前的小骰子面数之积,所以计算概率时可以不除骰子的面数.
最后合并到只剩余一个骰子就可以了.
这道题会爆 \(long\ long\) ,需要开 \(double\).
#include <bits/stdc++.h>
#define int long long
using namespace std;
typedef pair<double,double> PII;
PII r [1000000];
struct dice
{
double v[10000] = {0};
int minv;
int maxv;
};
dice create(int x)
{
dice t;
for (int i = 1; i <= x; i++)
{
t.v[i] = 1;
}
t.maxv = x;
t.minv = 1;
return t;
}
dice add(dice a, dice b)
{
dice t;
t.minv = a.minv + b.minv;
t.maxv = a.maxv + b.maxv;
for (int i = a.minv; i <= a.maxv; i++)
{
for (int j = b.minv; j <= b.maxv; j++)
{
t.v[i + j] += a.v[i] * b.v[j];
}
}
return t;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
queue<dice> q;
int x;
dice a, b, c, d, e;
a = create(4);
b = create(6);
c = create(8);
d = create(12);
e = create(20);
cin >> x;
for (int i = 1; i <= x; i++)
{
q.push(a);
}
cin >> x;
for (int i = 1; i <= x; i++)
{
q.push(b);
}
cin >> x;
for (int i = 1; i <= x; i++)
{
q.push(c);
}
cin >> x;
for (int i = 1; i <= x; i++)
{
q.push(d);
}
cin >> x;
for (int i = 1; i <= x; i++)
{
q.push(e);
}
while (q.size() > 1)
{
dice a = q.front();
q.pop();
dice b = q.front();
q.pop();
dice t = add(a, b);
q.push(t);
}
dice res = q.front();
int idx =0;
for (int i = res.minv; i <= res.maxv; i++)
{
r[idx++] = {res.v[i],i};
}
sort(r,r+idx,greater<PII>());
for(int i= 0;i<idx;i++)
{
cout<<r[i].second<<" ";
}
return 0;
}
\(\operatorname{Solution 2}\) 动态规划
设 \(dp[i][j]\) 表示前 \(i\) 个骰子(注意不是前 \(i\) 种.如果是前 \(i\) 种,则不能通过第 \(i\) 种的第 \(j-1\) 个骰子转移到第 \(j\) 个骰子) 掷总点数为 \(j\) 的情况数.
那么一定有 \(dp[i][j]\) = \(\sum^{dice[i]}_{s=1} dp[i-1][j-s]\) ,其中 \(dice[i]\) 代表第 \(i\) 个骰子的面数.
#include<bits/stdc++.h>
using namespace std;
int sides[6] = {0,4, 6, 8, 12, 20};
int dice[6];
long double dp[55][1000];
int sum[6];
vector<pair<long double, int>> res;
int main()
{
int n = 5;
for (int i = 1; i <= 5; i++)
{
cin >> dice[i];
sum[i]=sum[i-1]+dice[i];
}
memset(dp, 0, sizeof(dp));
dp[0][0] = 1;
int maxval = 0;
for (int i = 1; i <= 5; i++) maxval += dice[i] * sides[i];
for (int i = 1; i <= 5; i++)
{
for (int j = 1; j <= dice[i]; j++) // iterate over all dice
{
for (int val = 0; val <= maxval; val++)
{
for (int s = 1; s <= sides[i] && s <= val; s++)
{
dp[sum[i-1]+j][val] += dp[sum[i-1]+j-1][val - s];
}
}
}
}
for (int i = 0; i <= maxval; i++) if (dp[sum[n]][i] > 0) res.push_back({-dp[sum[n]][i], i});
sort(res.begin(), res.end());
for (auto p : res) cout << p.second << " ";
cout << endl;
}
由于每次循环只用到了当前层和上一层的值,因此可以采用类似背包的优化,把 \(dp\) 数组优化成一维.注意此时背包体积(掷得总点数)从大到小枚举.
for(int i=0; i<5; i++){
for(int j=0; j<dice[i]; j++){ // iterate over all dice
for(int val = maxval; val >= 0; val--){
dp[val] = 0;
for(int s = 1; s <= sides[i] && s <= val; s++) dp[val] += dp[val - s];
}
}
}
B.Balloon Darts
如果给出三个点的坐标,如何方便地判断三点共线?
三个点构造两个向量 \(A,B\) ,二维向量叉积 \(|A \times B|\) = \(x_1y_2 - x_2y_1\) ,结果为 \(0\) 则说明 \(A\) 与 \(B\) 平行
为了简化代码,可以引入 C++ 中的 complex 类.
复数的相乘法则.
因此只需要对向量 \(A\) 取共轭 ,然后与向量 \(B\) 乘得新的复数的虚部即为 \(x_1y_2 - x_2y_1\)
也即 imag(conj(A)*B)
ll cross(pt p, pt a, pt b) {return cross(a - p, b - p);}// 把三个点转化成两个向量
ll cross(pt a, pt b) {return imag(conj(a) * b);}//两个二位向量求叉积
\(\operatorname{Solution}\)
最开始给出 \(k=3\) 条直线,最开始所有的点都为未被直线穿过的点.
考虑有 \(k\) 条直线的情况,假设我们需要解决的点有 \(\infty\) 个.那么如果有解.不妨挑出 \(k+1\) 个点,这 \(k+1\) 个点一定可以组合出来合法解的其中一条边\(^{[1]}\).因此我们只需要枚举前 \(k+1\) 个点,看看他们组合出来的边后续能不能构造出来合法解就可以了.如果前 \(k+1\) 个点无论如何都找不到合法解,说明 \(k\) 条边一定无法把这些点全都囊括进来.
$[1]: $假如 \(k=3\) ,挑出来 \(4\) 个点,这 \(4\) 个点两两组合,可以组合出来 \(6\) 条边,如果这 \(6\) 条边都不在解里面,说明穿过这 \(4\) 个点的每条边都只会唯一的穿过这 \(4\) 个点的其中一个点.那么需要 \(4\)条边才可以,矛盾.因此证明这 \(6\) 条边一定存在解中的边. 而如果只选出来 \(3\) 个点或更少,是可以使得穿过这 \(3\) 个点中的每条边只通过这些点其中的一个点.得不到必要性.
只需要每次递归地处理.
首先对于 \(k\) 条边,从前 \(k+1\) 个点中选出两个点构成一条直线,然后对剩下所有未匹配的点挨个检查是否在这条线上.如果不在这条线上,则标记.
然后进入下一层递归 \(k-1\) 条边.对上一层对所标记的点,判断这些点能否全都被囊括.如果能则返回 \(true\) ,不能则再进行尝试.
#include <bits/stdc++.h>
using namespace std;
#define all(x) begin(x), end(x)
#define sz(x) (ll)(x).size()
using ll = long long;
using ld = long double;
using pt = complex<ll>;
constexpr ll tries[4] = {0, 1, 4*5, 9*5};
mt19937 rng(123456789);
ll random(ll l, ll r) {
return uniform_int_distribution<ll>(l, r-1)(rng);
}
// Kreuzprodukt, 0, falls kollinear.
ll cross(pt a, pt b) {return imag(conj(a) * b);}
ll cross(pt p, pt a, pt b) {return cross(a - p, b - p);}
bool solve(const vector<pt>& todo, ll depth = 3) {
if (sz(todo) <= 2*depth) return true;
for(int i = 0;i<depth+1;i++)
{
for(int j = i+1;j<depth+1;j++)
{
vector<pt> remain;
for(pt c:todo)
{
if (cross(todo[i], todo[j], c) != 0) remain.push_back(c);
}
if (solve(remain, depth - 1)) return true;
}
}
return false;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
ll n;
cin >> n;
vector<pt> in(n);
for (ll i = 0; i < n; i++) {
ll x, y;
cin >> x >> y;
in[i] = {x, y};
}
if (solve(in)) {
cout << "possible" << endl;
} else {
cout << "impossible" << endl;
}
}
另外还可以用随机化算法,每次随机地挑选出来两条边.尝试一定次数后如果还是没有合法解,那么返回 \(false\) .
- Recursively select a random line through two points.
- At step k check if the chosen line covers \(1/k\) of all points.
Yes: recursively continue with k − 1 and the remaining points.
No: try another line or abort after sufficient many tries (∼ 5 · k).- Time complexity for (k = 3): n · 5 · k! = 30 · n
bool solve(const vector<pt>& todo, ll depth = 3) {
if (sz(todo) <= 2*depth) return true;
for (ll i = 0; i < tries[depth]; i++) {
ll j = random(0, sz(todo));
ll k = j;
while (k == j) k = random(0, sz(todo));
vector<pt> remain;
for (pt c : todo) {
if (cross(todo[j], todo[k], c) != 0) remain.push_back(c);
}
if (solve(remain, depth - 1)) return true;
}
return false;
}
C.Cosmic Commute
给出一个无向连通图,图中相连的各点边权均为 \(1\) ,其中有 \(k\) 个点,这 \(k\) 个点可以瞬时随机传送(不需要花费)到其他剩余的 \(k-1\) 个点,也就是不可能传送到自己.
最多使用一次传送(当然也可以不使用传送),请求出 \(1\sim n\) 的最小期望距离(花费).
\(\operatorname{Solution}\)
设 \(W\) 是一个集合,存储了所有可以传送的 \(k\) 个点. \(dist[i]\) 表示 \(1\sim i\) 的最短距离, \(dist\_ re[i]\) 表示 \(n\sim i\) 的最短距离.
按照题意一步一步做就好了,假设我们在点 \(i\) 处发起了传送操作,那么是随机到其他的 \(k-1\) 个点,不妨设传送到了 \(j\) 点,那么这次的 \(res = dist[i] + dist\_ re[j]\) ,概率 \(p = \frac{1}{k-1}\) .那么在点 \(i\) 发起传送,到终点的花费,其期望就是 \(\sum^{k}_{j=1}(dist[i] + dist\_ re[j])\ \ \ (j\neq i)\)
\(=dist[i]*(k-1)+\sum dist\_ re[j]\)
那么答案就是 \(\min(dist[i]+\sum^{k}_{j=1}dist\_ re[j]) \times \frac{1}{k-1}\) (其中 \(i,j \in W\) 且 \(i\neq j\) 或 \(dist[n]/1\) )
由于边权为 \(1\) 因此最短路直接用 \(BFS\) 即可求解.
另外,如果直接枚举传送的起点和终点 \(i,j\) ,时间将会达到 \(O(n^2)\) .
int fz = 0, fm = 1;
double res = 1e16;
for (int i = 1; i <= k; i++)
{
double temp = 0;
for(int j =1;j<=k;j++)
{
if(i==j)continue;
temp+=dist_re[K[j]];
}
if (res > temp / (k - 1) + dist[K[i]])
{
res = temp / (k - 1) + dist[K[i]];
fz = temp + dist[K[i]] * (k - 1), fm = k - 1;
int g = __gcd(fz, fm);
fz /= g, fm /= g;
}
}
观察到求解 \(dist\_ re\) 过程用了大量重复,因此可以先统计 $S = \sum^{k}_{i=1} dist _ \ re[i] $,然后如果以 \(i\) 为起点,那么 \(S\) 减去 \(dist\_ re[i]\) 即可
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<iostream>
#include<queue>
#include<algorithm>
#define endl '\n'
#define pb push_back
#define int long long
using namespace std;
typedef pair<int, int> PII;
const int N = 2e5 + 10;
int n, m, k;
int K[N];
vector<int> v[N];
int dist[N];
int dist_re[N];
int vis[N];
int bfs(int u, int dis[])
{
queue<PII> q;
q.push({0, u});
memset(dis, 0x3f, N*sizeof(int));
dis[u] = 0;
while (q.size())
{
PII t = q.front();
int sz = v[t.second].size();
q.pop();
for (int i = 0; i < sz; i++)
{
if (dis[v[t.second][i]] > dis[t.second] + 1)
{
dis[v[t.second][i]] = dis[t.second] + 1;
q.push({dis[v[t.second][i]], v[t.second][i]});
}
}
}
return 0;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m >> k;
for (int i = 1; i <= k; i++)
{
cin >> K[i];
}
for (int i = 1; i <= m; i++)
{
int a, b;
cin >> a >> b;
v[a].pb(b);
v[b].pb(a);
}
bfs(1, dist);
bfs(n, dist_re);
double res = 1e16;
int fz = 0, fm = 1;
int S = 0;
for (int i = 1; i <= k; i++)
{
S += dist_re[K[i]];
}
for (int i = 1; i <= k; i++)
{
double temp = S - dist_re[K[i]];
if (res > temp / (k - 1) + dist[K[i]])
{
res = temp / (k - 1) + dist[K[i]];
fz = temp + dist[K[i]] * (k - 1), fm = k - 1;
int g = __gcd(fz, fm);
fz /= g, fm /= g;
}
}
if (res > dist[n])
{
cout << dist[n] << "/1" << endl;
}
else
{
cout << fz << "/" << fm << endl;
}
return 0;
}