link: 牛客
vp time: 250502 12:00-17:00
team: std::future
members: LittleYoo, Broken_Eclipse, f2021ljh
accepted: LEIKBCG
penalty: 1193
ranking: 26/238 (10.92%)
problem difficulty: LEIKBGCDFJAMH
L. Polygon(签到题)
给定
条边的长度,判断能否构成一个简单多边形。
多边形不等式:每条边小于其他所有边之和。
故判断最大的边是否满足即可。
E. Plus(结论题)
给定
,求所有
满足
,
均为素数,且
为素数。
。
如果 均为奇数或均为
,则
一定为偶数,故不为素数。
因此,满足条件的 只有
,
且为素数,且
为素数。
进一步发现,,又因为
为奇素数,若
则有
且
,故
,因此
。
综上, 为唯一的解。
I. Generator(期望 DP,Euler 常数)
给定一个正整数
(
),每次操作在
内均匀随机生成一个整数,并将
变成这个整数。
求将
变成
的期望操作次数。误差不超过
即可。
设 为将
变成
的期望操作次数。当
时,有
移项得
同理有
即
相减得
根据题意,初值 。故
当 时,可
计算。
当 时,可以证明
与
的差是收敛的。
证明(梅加强):由
,且数列
递增,
递减,且
收敛于
,有不等式
两边取对数得
即
对于
,将上述不等式相加,得
令
,则由右端不等式知
,且有
故由单调有界准则知,
收敛,其极限记为
,称为 Euler 常数,计算表明
可打表计算 的近似值,然后用
近似。
取 即可。
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
using namespace std;
typedef double db;
int const K = 1e7;
db const _gamma = 0.577215665402;
int n;
db ans = 1;
signed main() {
cin >> n;
if (n <= K) {
f(i, 1, n - 1) ans += 1.0 / i;
} else {
ans += _gamma + log(n - 1);
}
cout << fixed << setprecision(10) << ans << '\n';
// int const N = 1e9;
// db sum = 0;
// f(i, 1, N) sum += 1.0 / i;
// db err = sum - log(N);
// cout << fixed << setprecision(12) << err << '\n';
return 0;
}
由于要求的精度不高,在 很大的情况下,可以将
近似为
,这样复杂度是
,取
即可通过。
#include <cstdio>
#include <algorithm>
#define ll long long
#define Reg register
using namespace std;
const int maxn = 210000;
int n;
double ans;
int main() {
scanf("%d", &n);
if (n == 1) ans = 1;
else if (n == 2) ans = 2;
else {
ans = 2;
int up = n - 1;
double a = 0, b = 0;
if (up <= 100000000 - 1) {
for (Reg int i = 2; i <= up; ++i) ans += 1.0 / i;
}
else {
int maxx = 100000000 - 1;
for (Reg int i = 2; i <= maxx; ++i) ans += 1.0 / i;
for (Reg int i = maxx + 1; i <= up; i += 4) ans += 4.0 / i;
}
}
printf("%.10lf\n", ans);
return 0;
}
由于 ,也可采用分段打表的方式。(我不会)
K. Maze(BFS)
给定一个
的网格,有一些网格是障碍。现在要从
走到
,要求:
- 每次可以向上下左右走一步;
- 不能经过障碍;
- 沿同一方向连续走不超过
步。
求最少步数。如果不能到达,输出
。
数据组数
。
,
。保证
的数据组数不超过
。
记状态 为当前在
网格,且在
方向上已经连续移动了
次,其中
,分别表示上下左右。
设 为该状态下的最少步数,直接进行 BFS 即可,松弛操作类似最短路。
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
using namespace std;
int const INF = 0x3f3f3f3f;
int const N = 110;
int const dx[4] = {-1, 1, 0, 0};
int const dy[4] = {0, 0, -1, 1};
int tt, n, m, d[N][N][N][4];
char g[N][N];
struct Node {
int x, y, k, dir;
};
void bfs() {
memset(d, 0x3f, sizeof d);
queue<Node> q;
q.push({1, 1, 0, 0});
d[1][1][0][0] = 0;
while (!q.empty()) {
auto [x, y, k, dir] = q.front();
q.pop();
f(i, 0, 3) {
if (i == dir && k == m) continue;
int xx = x + dx[i], yy = y + dy[i];
if (xx < 1 || xx > n || yy < 1 || yy > n || g[xx - 1][yy - 1] == '*') continue;
int nxk = (i == dir ? k + 1 : 1);
int cur = d[x][y][k][dir], &nxt = d[xx][yy][nxk][i];
if (cur + 1 < nxt) {
nxt = cur + 1;
q.push({xx, yy, nxk, i});
}
}
}
int ans = INF;
f(i, 0, 4) f(j, 0, m) ans = min(ans, d[n][n][j][i]);
if (ans == INF) cout << "-1\n";
else cout << ans << '\n';
return;
}
void solve() {
cin >> n >> m;
f(i, 0, n - 1) cin >> g[i];
bfs();
return;
}
signed main() {
cin.tie(0)->sync_with_stdio(false);
cin >> tt;
while (tt--) solve();
return 0;
}
B. Capital Program(树的中心,贪心)
给定一棵
个节点的树,边权为
。
要求在树中选定一个大小为
的连通块,使得连通块外所有点到连通块的距离的最大值最小。
输出这个最小值。
,
。
如果 ,则一定要选树的中心(直径的任意一个中点)作为首都。
以中心为根,向下选 个点,根据贪心的思想,不断选与当前首都有连边的点中,子树深度最深的那个。
因此求出中心,以中心为根进行 DFS,求出每个节点的子树深度。
可以发现,选定的节点的子树深度一定是单调不增的。因此直接将子树深度按降序排序,那么第 个即为最后一个选择的节点,故第
个为其他节点到连通块的最远距离。
时间 。
如何求树的中心?
下面用类似拓扑排序的方法找到树的中心,即:开始时将叶子节点放进队列,从树中删去队首并出队的同时,向队列中不断加入新的叶子,最后一个入队的即为中心。
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define pb push_back
using namespace std;
typedef pair<int, int> pii;
int const N = 1e5 + 10;
int n, k;
vector<int> g[N];
bitset<N> used;
int deg[N];
int getCenter() {
queue<int> q;
f(i, 1, n) {
if ((deg[i] = g[i].size()) == 1) {
q.push(i);
used[i] = 1;
}
}
int u;
while (!q.empty()) {
u = q.front();
q.pop();
for (int v: g[u]) {
if (!used[v] && (--deg[v] == 1)) {
q.push(v);
used[v] = 1;
}
}
}
return u;
}
int dep[N];
void dfs2(int u, int fa) {
for (int v: g[u]) if (v ^ fa) {
dfs2(v, u);
dep[u] = max(dep[u], dep[v]);
}
++dep[u];
return;
}
signed main() {
cin.tie(0)->sync_with_stdio(false);
cin >> n >> k;
f(i, 2, n) {
int u, v;
cin >> u >> v;
g[u].pb(v), g[v].pb(u);
}
int cen = getCenter();
dfs2(cen, 0);
sort(dep + 1, dep + n + 1, greater<int>());
cout << dep[k + 1] << '\n';
return 0;
}
或者可以用换根 DP 的方式求中心,即求到其他点的最大距离最小的点。
int dup[N], ddn[N], ddn2[N], msn[N], msn2[N];
void dfs(int u, int fa) {
for (int v: g[u]) if (v ^ fa) {
dfs(v, u);
if (ddn[v] + 1 >= ddn[u]) {
ddn2[u] = ddn[u], msn2[u] = msn[u];
ddn[u] = ddn[v] + 1, msn[u] = v;
} else if (ddn[v] + 1 > ddn2[u]) {
ddn2[u] = ddn[v] + 1, msn2[u] = v;
}
}
return;
}
void dfs1(int u, int fa) {
for (int v: g[u]) if (v ^ fa) {
if (msn[u] == v) dup[v] = 1 + max(dup[u], ddn2[u]);
else dup[v] = 1 + max(dup[u], ddn[u]);
dfs1(v, u);
}
return;
}
int getCenter() {
dfs(1, 0);
dfs1(1, 0);
int res = 1, mn = ddn[1];
f(i, 2, n) {
int cur = max(ddn[i], dup[i]);
if (cur < mn) {
mn = cur, res = i;
}
}
return res;
}
C. Segment Tree(思维题;贪心;DP)
下面用
代替算法伪代码中的
。
对于长度为
的正整数序列
,其中
,执行上述算法,将序列插入动态开点线段树。
问如何构造序列
,使得节点数
取到最大值?输出这个最大值。
数据组数
,
。
设 。
一棵线段树的形状一定是一个深度为 的满二叉树,在其基础上选择
个叶子,延伸出一层的左右儿子。
为了使选择的路径中包含尽可能多的未创建过的节点,应该左、右子树交替选择。
……(推出一个神必式子)
答案为
G. Hot Water Pipe(队列,暴力)
有一根热水管,由
节组成,编号为
到
,每节容量为
单位体积。
第
节水管内的热水的初始温度为
,且每分钟会下降
单位温度。
任一节水管中的热水温度一旦小于
,就会瞬间被加热至
,加热时间可忽略不计。
使用热水时,从第
节水管中取出
单位体积热水,同时前
节水管中的热水向后平移一节(温度不变),并在第一节水管中注入温度为
的热水。
有
次取水操作,每次操作在上次操作的第
分钟后,且使用了
单位体积的热水。
输出每次取出的热水温度的总和。
,
,
。
注意到,热水的温度是周期性变化的,所以如果知道初始温度和经过的时间,就可以知道最终的温度。
另外,每次取水操作中,新加入的 体积水的温度,无论何时总是相同的。
取水操作与加水操作的规则,符合队列的性质,故用队列来维护,元素为加水的体积和时间。
可以发现,在若干次取水操作之后,再取出的水,一定是后加入的水,而任意后加入的水的加入时间都是已知的,从而可以求出答案。
因此直接暴力模拟取水和加水的过程。
队列中同时最多有 个元素,每次弹出至多
个元素,加入
个元素,由势能分析知,时间复杂度是
的。
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define g(x, y, z) for (int x = (y); x >= (z); --x)
#define fi first
#define se second
#define mp make_pair
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
int const N = 1e6 + 10;
int n, m, tmin, tmax, dt, a[N];
queue<pii> q;
int get_t(ll tim, int t0) {
int t = t0 - tim % dt;
return t < tmin ? t + dt : t;
}
int get_t0(ll tim) {
return tmin - 1 + tim % dt;
}
signed main() {
cin.tie(0)->sync_with_stdio(false);
cin >> n >> m >> tmin >> tmax;
dt = tmax - tmin + 1;
f(i, 1, n) cin >> a[i];
g(i, n, 1) q.push(mp(a[i], 1));
ll tim = 0;
while (m--) {
int t, k;
ll tot = 0;
cin >> t >> k;
int kk = k;
tim += t;
while (!q.empty() && q.front().se <= k) {
k -= q.front().se;
tot += get_t(tim, q.front().fi) * 1ll * q.front().se;
q.pop();
}
if (q.empty() && k) {
tot += 1ll * k * tmax;
} else if (q.front().se > k) {
q.front().se -= k;
tot += get_t(tim, q.front().fi) * 1ll * k;
}
q.push(mp(get_t0(tim), min(kk, n)));
cout << tot << '\n';
}
return 0;
}
*D. Game(博弈,结论)
有
堆石子,第
堆石子有
颗。Alice 和 Bob 轮流执行以下操作:
- 选择一堆非空的石子。
- 将若干颗石子从这堆中拿出,可以拿走全部,但不能不拿。
- 对这堆中剩余的每个石子,可以选择不动,也可以选择放到另一堆非空的石子中。
次询问,每次询问给定
,问用
进行游戏,谁有必胜策略?
,
。
下面证明:如果所有石堆的石子数的出现次数都为偶数,则先手必败,否则先手必胜。
-
如果所有石堆的石子数的出现次数都为偶数,那么将所有石堆分成完全相同的两组,后手可以选择在另一组跟随先手的一切操作,最后清空的一定是后手,故后手必胜。
-
如果存在一堆石子,石子数的出现次数为奇数:不妨设
。
-
若
,则将
取走,同时对于所有
,在
上放
个石子,由于
这样做是可行的,得到的序列为
,故出现次数均为偶数。
-
若
,则从
中取走
个石子,同时对于所有
,在
上放
个石子,类似地,可以证明,这样做是可行的,得到的序列为
,故出现次数均为偶数。
-
对于静态询问区间内每个数的出现次数,可以用莫队实现。时间复杂度 。
然而我们不需要求出每个数的具体出现次数,只需要求是否有数字出现过奇数次。因此可以用 异或哈希 的 trick 来做:给每个数随机赋一个 64 位权值,求异或和,如果为零,则所有数出现的次数都是偶数,否则存在至少一个数出现了奇数次。

京公网安备 11010502036488号