B
dfs
给一个六面都决定的骰子,现在创造一个新骰子,要求六面的和是n,问两个骰子都随机投,一共36种情况,构造一个方案使得至少赢19种
第一个骰子六面的值很大,朴素的枚举第二个骰子每一面的值复杂度太大。但实际上我们只用考虑是否能赢第一个骰子,所以第二个骰子可以只枚举每一面,能赢第一个骰子的几面。把第一个骰子的值升序排序,假设能赢前iii面,则第二个骰子的值可以直接设为为ai+1a_i+1ai+1,这是赢前iii面花费最小的方式。
最后六面都枚举出来了,如果还有剩余的点数,随便加到某一个骰子上,情况不会变的更差
严格来说第二个骰子6面,每一面都有7种选择(大于第一个骰子前1-6面,或者一面也不大于),总计算量76=117,6497^6=117,64976=117,649,可以接受。
c
void solve()
{
vi a(10);
rep(i, 1, 6)
{
cin >> a[i];
}
sort(a.begin() + 1, a.begin() + 7);
int k;
cin >> k;
vi b(10);
auto &&dfs = [&](auto &&dfs, int j, int sum, int cnt) -> void
{
// cout << j << ' ' << cnt << '\n';
if (sum > k)
{
return;
}
if (j == 7)
{
if (cnt >= 19)
{
// cout << cnt << ' ';
cout << "YES\n";
b[6] += k - sum;
rep(i, 1, 6)
{
cout << b[i] << ' ';
}
exit(0);
}
return;
}
rep(i, 1, 6)
{
b[j] = a[i] + 1;
dfs(dfs, j + 1, sum + b[j], cnt + i);
}
b[j] = 1;
dfs(dfs, j + 1, sum + 1, cnt);
};
dfs(dfs, 1, 0, 0);
cout << "NO\n";
}
C
数学 解方程
一个线段以u的速度水平经过,线段两个端点在t时刻的坐标为(xi+ut,y)(x_i+ut,y)(xi+ut,y),现在我们从原点出发,速度v,可以朝任意方向,问不撞到线段(可以与线段端点重合)的前提下,到达y=wy=wy=w的最短时间
虽然我们能走任意路线,但是走曲线时不优的,最优解应该要么是可以沿着y轴直接向上,要么被线段挡住的话,应该是先走一个斜线,和线段的某个端点同时到达一个位置,在向上走直线。
直接走直线的答案是显然的w/vw/vw/v,条件是竖着往上走到y的位置时,线段的两个端点形成的x区间不包含0。对于被挡住的情况,要和线段端点重合,和左右哪个相遇是最优的不一定,可以两个都算了,取max。考虑到两个端点的算法完全一样只是参数不同,写个函数复用
首先和线段端点同时到达,方程为(x+ut)2+y2=(vt)2(x+ut)^2+y^2=(vt)^2(x+ut)2+y2=(vt)2,转换成ttt为主元的一元二次方程形式为(u2−v2)t2+2xut+(x2+y2)=0(u^2-v^2)t^2+2xut+(x^2+y^2)=0(u2−v2)t2+2xut+(x2+y2)=0
接下来提出来A=u2−v2,B=2xu,C=x2+y2A=u^2-v^2,B=2xu,C=x^2+y^2A=u2−v2,B=2xu,C=x2+y2三个参数,用一元二次求根公式即可。有一些分类讨论
- A为0,B不为0,退化成一元一次方程
- AB都为0,无解
- AB都不为0,看δ=B2−4AC\delta=B^2-4ACδ=B2−4AC,小于0也无解
- 否则有两个解,解的含义是时间,负数无意义,等价于无解,两个解取最小值即为答案
- 中间所有无解的地方,为了代码简单,都可以直接返回无穷大,最后两个端点分别调用这个函数取最小值。
c
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
cout << setprecision(15);
while (T--) {
double w, x1, x2, y, u, v;
cin >> w >> x1 >> x2 >> y >> u >> v;
auto calc = [&](double x) -> double {
double A = v * v - u * u;
double B = -2.0 * u * x;
double C = -(x * x + y * y);
const double INF = numeric_limits<double>::infinity();
if (A == 0) {
if (B == 0) return INF;
double ans = -C / B;
return ans > 0 ? ans : INF;
}
double delta = B * B - 4.0 * A * C;
if (delta < 0) return INF;
double r1 = (-B + sqrt(delta)) / (2.0 * A);
double r2 = (-B - sqrt(delta)) / (2.0 * A);
return min(r1 > 0 ? r1 : INF, r2 > 0 ? r2 : INF);
};
double ans;
if ((x1 * v + u * y < 0 && 0 < x2 * v + u * y)) {
ans = w / v;
} else {
ans = min(calc(x1), calc(x2));
ans += (w - y) / v;
}
cout << ans << "\n";
}
return 0;
E
异或哈希
一个长度n的排列,所有两个元素间的空隙,如果三个人中至少有两个同意,左侧的题都比右侧的简单,就可以在这个空隙处做一次划分。三个人,每个人给的都是一个排列,表示他们认为的题目从简单到难的排序,问最后多少个位置被划分
三个人中任意两个都能决定划分。因此枚举所有两个人的组合,有三种,对于每种组合,考虑划分。这可以写个函数复用
函数内部,考虑两个人的排列的前缀1,i1,i1,i,如果两个人的前缀形成的集合是相等的,也就是两个人认为的前iii简单的题是一样的,则这里就存在了一个,至少有两个人认为,左侧题目比右侧简单的划分,在一个答案数组上记录,最后统计这个答案数组上有多少个空隙有记录,就是答案
关键是如何判断两个前缀形成的集合是否相等,这里是个排列,没有重复元素的集合判等,可以用异或哈希,先用随机数接口给每个元素映射到一个随机值,然后用一个集合内元素的随机值的异或和,表示这个集合,如果两个前缀异或和相等就可以认为集合相等。
c
const int N = 2e5 + 10;
int a[N][3];
int b[N];
mt19937_64 rnd(chrono::steady_clock::now().time_since_epoch().count());
void solve()
{
int n;
cin >> n;
rep(i, 1, n)
{
b[i] = rnd();
}
rep(i, 0, 2)
{
rep(j, 1, n)
{
cin >> a[j][i];
}
}
vi cnt(n + 10);
auto work = [&](int p, int q)
{
int h1 = 0, h2 = 0;
rep(i, 1, n)
{
h1 ^= b[a[i][p]];
h2 ^= b[a[i][q]];
if (h1 == h2)
{
cnt[i]++;
}
}
};
work(0, 1);
work(0, 2);
work(1, 2);
int ans = 0;
cnt[n]++;
rep(i, 1, n)
{
if (cnt[i])
{
ans++;
}
}
cout << ans << '\n';
}
H
通信
开始给一个长度n排列,第一次输出一个n * n矩阵,要求每一行都是一个排类,且每一行互不相同。第一次输出的矩阵把行打乱,作为第二次运行的输入,需要还原最开始的排列。
做法很多,官方题解是最简单的。由于只打乱了矩阵的行,没打乱列,每列的集合是不变的。那么可以让第i列的集合的众数是pi。
其实n行都直接输出原始排列就能保证第i列的众数是pi,但是还要满足每一行的排列互不相同。做一些微小的改变让每一行互不相同,同时不破坏每一列的众数即可。一种做法是第i行交换p1,pip_1,p_ip1,pi。这样在n行里,n-1行的第i列还是pi,只有第i行的第i列变成了p1,第i列的众数仍然是pi。
这样对于2-n行都可以用众数找到pi。对于第一列,是一个1-n的排列。得不到什么信息。但是p2-pn都确定了,剩余那个没出现的元素就是p1
c
void solve(void) {
string s;
cin >> s;
int n;
cin >> n;
if (s == "first") {
vi a(n + 1);
rep(i, 1, n) {
cin >> a[i];
}
rep(i, 1, n) {
swap(a[i], a[1]);
rep(j, 1, n) {
cout << a[j] << ' ';
}
swap(a[i], a[1]);
cout << '\n';
}
} else {
vvi a(n + 1, vi(n + 1));
rep(i, 1, n) {
rep(j, 1, n) {
cin >> a[i][j];
}
}
vi vis(n + 1);
vi ans(n + 1);
rep(j, 2, n) {
vi cnt(n + 1);
rep(i, 1, n) {
cnt[a[i][j]]++;
}
pii mx = {0, 0};
rep(i, 1, n) {
mx = max(mx, {cnt[i], i});
}
ans[j] = mx.second;
vis[mx.second] = 1;
}
rep(i, 1, n) {
if (!vis[i]) {
ans[1] = i;
}
}
rep(i, 1, n) {
cout << ans[i] << ' ';
}
}
}
J
二分 贪心 倍增
一个数组a首先可以做一次循环移位。然后划分成最多k段,每一段求和记为序列b,若长度为m,则对b数组跑一个m级流水线,一个元素x在每一级的耗时都是x。问流水线处理完所有元素的最短时间?
手玩或者推式子可以发现,对于一个确定的数组a,流水线的耗时是∑ai+max(bi)\sum a_i+\max(b_i)∑ai+max(bi),也就是a数组元素和,加上b数组最大值(a中元素和最大的一段)
现在就是要最小化a中划分出的最大段的和,考虑二分这个最大段。check内部贪心地检验,能否在每一段都不超过mid的情况下,划分不超过k段。以上是不考虑循环移位,考虑到循环移位,可以转化成把a数组在后面拼接一遍,在这个长度2n的数组上,考虑所有长度n的子数组,只要存在一个子数组能划分不超过k段,check就有解。
如果没有循环移位,贪心划分即可,O(n)O(n)O(n)的。现在有循环移位,要对n个子数组分别检查,如果每个都做一次贪心,check复杂度O(n2)O(n^2)O(n2),不可接受。
划分k段可以看成跳k次,一个长度n的子数组i,i+n−1i,i+n-1i,i+n−1能否划分成不超过k段,等价于跳k次能否到达至少i+ni+ni+n的位置。以低于O(n)O(n)O(n)的复杂度检查跳k次最远能到哪里,这是经典倍增。
首先划窗找到对于每个lll,sum(l,r)≤midsum(l,r)\le midsum(l,r)≤mid的最远rrr在哪,l,rl,rl,r就是lll为左端点能划分的最大区间,看成(l,r+1)(l,r+1)(l,r+1)建边。作为从lll出发跳20=12^0=120=1次最远能到的位置,接着倍增即可。
检查时,对于每个子数组i,i+n−1i,i+n-1i,i+n−1,以iii为起点,倍增跳kkk步,计算能到的最远位置,看是否不小于i+ni+ni+n,倍增跳的复杂度O(logn)O(\log n)O(logn)。
check复杂度O(nlogn)O(n\log n)O(nlogn),整体复杂度O(nlog2n)O(n\log^2n)O(nlog2n)
在实现上,如果一个区间l,r,r=2nl,r,r=2nl,r,r=2n,那么建边会建一个(l,2n+1)(l,2n+1)(l,2n+1),超出1,2n1,2n1,2n范围。这是可以的,但到了2n+12n+12n+1这个点,就只能待在这个点了,所以倍增时初始化f(0,2n+1)=2n+1f(0,2n+1)=2n+1f(0,2n+1)=2n+1,形成一个自环。并且倍增预处理时也要计算2n+12n+12n+1这个位置,保证f(i,2n+1)f(i,2n+1)f(i,2n+1)的值都是正确的
倍增跳k步时,从高到低检查k的二进制位,如果第i位为1则跳2i2^i2i步,从xxx到f(i,x)f(i,x)f(i,x)的位置
c
int f[22][N << 1];
void solve(void) {
int n, m, k;
cin >> n >> m >> k;
vi a(2 * n + 1);
int sum = 0;
rep(i, 1, n) {
cin >> a[i];
sum += a[i];
a[i + n] = a[i];
}
int l = 1, r = sum;
int res = -1;
int mx = __lg(n) + 1;
auto check = [&](int mi)->bool{
int sum = 0;
f[0][2 * n + 1] = 2 * n + 1;
for (int l = 2 * n, r = 2 * n; l >= 1; l--) {
sum += a[l];
while (sum > mi) {
sum -= a[r];
r--;
}
f[0][l] = r + 1;
}
rep(i, 1, mx) {
rep(j, 1, 2 * n + 1) {
f[i][j] = f[i - 1][f[i - 1][j]];
}
}
rep(i, 1, n) {
int x = i;
for (int j = mx; j >= 0; j--) {
if (k >> j & 1) {
x = f[j][x];
}
}
if (x >= i + n) {
return 1;
}
}
return 0;
};
while (l <= r) {
int mi = (l + r) / 2;
if (check(mi)) {
res = mi;
r = mi - 1;
} else {
l = mi + 1;
}
}
int ans = sum + (m - 1) * res;
cout << ans << '\n';
}
K
期望DP 最短路计数
问起点走到终点的路程的期望。对于每个点,如果有网,则会根据出边连接点到达终点的最短路个数加权,然后随机选取,也就是比如(1,2)(1,3)(1,2)(1,3)(1,2)(1,3),2,3到终点的最短路分别有4,5条,则走这两条边的概率分别为4/9,5/9。如果没网,则所有出边等概率随机走。保证图是个DAG
考虑期望dp,如果u能走到v,则通过dpvdp_vdpv转移到dpudp_udpu,也就是需要先算出dpvdp_vdpv。因此我们的dp顺序是把原图边取反,跑一个拓扑排序。原图是个DAG,且联通,取反了肯定还是DAG且联通。
转移时,没网显然是
dpu=(∑dpv+w(u,v))/m dp_u=(\sum dp_v+w(u,v))/m dpu=(∑dpv+w(u,v))/m
其中m是u的出边个数。
有网则是
dpu=(∑cntv×(dpv+w(u,v)))/m dp_u=(\sum cnt_v\times(dp_v+w(u,v)))/m dpu=(∑cntv×(dpv+w(u,v)))/m
其中cntvcnt_vcntv表示v到终点的最短路个数
这个转移按拓扑序转移即可。还需要一个最短路计数,这也是典,我们在dij的同时,除了维护最短路disidis_idisi,还维护一个最短路计数cnticnt_icnti。如果一个松弛比原来更短,则覆盖原来的cntcntcnt,如果松弛和原来的距离相等,则加入原来的cntcntcnt
c
const int N = 2e5 + 10;
const int INF = 1e18;
const int M = 998244353;
#define pii pair<int, int>
int qpow(int a, int b)
{
int res = 1;
while (b)
{
if (b & 1)
{
res = res * a % M;
}
a = a * a % M;
b >>= 1;
}
return res;
}
int inv(int x)
{
return qpow(x, M - 2);
}
void solve()
{
int n, m, t;
cin >> n >> m >> t;
string s;
cin >> s;
s = ' ' + s;
vector<vector<pii>> g(n + 10), G(n + 10);
vi in(n + 10);
rep(i, 1, m)
{
int u, v, w;
cin >> u >> v >> w;
g[u].push_back({v, w});
G[v].push_back({u, w});
in[u]++;
}
vi dis(n + 10, INF), cnt(n + 10);
priority_queue<pii, vector<pii>, greater<pii>> pq;
pq.emplace(0, t);
dis[t] = 0;
cnt[t] = 1;
while (pq.size())
{
auto [d, u] = pq.top();
pq.pop();
if (d > dis[u])
{
continue;
}
for (const auto &[v, w] : G[u])
{
int nd = dis[u] + w;
if (nd < dis[v])
{
dis[v] = nd;
cnt[v] = cnt[u];
pq.emplace(nd, v);
}
else if (nd == dis[v])
{
cnt[v] += cnt[u];
cnt[v] %= M;
}
}
}
queue<int> q;
q.push(t);
vi dp(n + 10);
while (q.size())
{
int u = q.front();
q.pop();
for (auto [v, w] : G[u])
{
if (--in[v] == 0)
{
q.push(v);
}
}
if (s[u] == '1')
{
int tot = 0;
int sum = 0;
for (auto [v, w] : g[u])
{
if (dis[v] + w != dis[u])
continue;
sum += cnt[v] * (dp[v] + w) % M;
tot += cnt[v];
}
tot %= M;
sum %= M;
// cout << u << ' ' << sum << ' ' << tot << '\n';
sum = sum * inv(tot) % M;
dp[u] = sum;
}
else
{
int tot = 0;
int sum = 0;
for (auto [v, w] : g[u])
{
sum += dp[v] + w;
tot++;
}
tot %= M;
sum %= M;
sum = sum * inv(tot) % M;
dp[u] = sum;
}
}
rep(i, 1, n)
{
cout << dp[i] << ' ';
}
}