2023-12-30 训练总结
返回 C 组做题,然后发现自己挂分了。
T1 寻找道路
[NOIP2014 提高组] 寻找道路
题目背景
NOIP2014 提高组 D2T2
题目描述
在有向图 \(G\) 中,每条边的长度均为 \(1\),现给定起点和终点,请你在图中找一条从起点到终点的路径,该路径满足以下条件:
- 路径上的所有点的出边所指向的点都直接或间接与终点连通。
- 在满足条件 \(1\) 的情况下使路径最短。
注意:图 \(G\) 中可能存在重边和自环,题目保证终点没有出边。
请你输出符合条件的路径的长度。
输入格式
第一行有两个用一个空格隔开的整数 \(n\) 和 \(m\),表示图有 \(n\) 个点和 \(m\) 条边。
接下来的 \(m\) 行每行 \(2\) 个整数 \(x,y\),之间用一个空格隔开,表示有一条边从点 \(x\) 指向点 \(y\)。
最后一行有两个用一个空格隔开的整数 \(s,t\),表示起点为 \(s\),终点为 \(t\)。
输出格式
输出只有一行,包含一个整数,表示满足题目描述的最短路径的长度。如果这样的路径不存在,输出 \(-1\)。
样例 #1
样例输入 #1
3 2 1 2 2 1 1 3 样例输出 #1
-1 样例 #2
样例输入 #2
6 6 1 2 1 3 2 6 2 5 4 5 3 4 1 5 样例输出 #2
3 提示
样例 1 解释
如上图所示,箭头表示有向道路,圆点表示城市。起点 \(1\) 与终点 \(3\) 不连通,所以满足题目描述的路径不存在,故输出 \(-1\)。
样例 2 解释
如上图所示,满足条件的路径为 \(1\to 3\to 4\to 5\)。注意点 \(2\) 不能在答案路径中,因为点 \(2\) 连了一条边到点 \(6\),而点 \(6\) 不与终点 \(5\) 连通。数据范围及约定
- 对于 \(30\%\) 的数据,\(0<n\le10\),\(0<m\le 20\)。
- 对于 \(60\%\) 的数据,\(0<n\le100\),\(0<m\le 2000\)。
- 对于 \(100\%\) 的数据,\(0<n\le 10^4\),\(0<m\le 2\times 10^5\),\(0<x,y,s,t\le n,x,s\ne t\)。
反向建边,跑 dfs 算出能到达终点的点,然后跑 dij 就可以了。
/** * @file 寻找道路.cpp * @tag: #GMOJ#最短路 * @author: ZnPdCo * @date: 2023-12-30 14:26:11 * @problem: https://gmoj.net/senior/#main/show/3934 **/ #include <cstdio> #include <queue> #define ll long long #define N 10010 #define M 200010 using namespace std; ll n, m, st, ed; bool flag[N], go[N]; ll head[N]; ll nxt[2*M]; bool type[2*M]; ll to[2*M], cnt; void addEdge(ll u, ll v, ll t) { cnt++; to[cnt] = v; nxt[cnt] = head[u]; type[cnt] = t; head[u] = cnt; } void dfs(ll u) { flag[u] = 1; for(ll i = head[u]; i; i = nxt[i]) if(type[i] == 1) { ll v = to[i]; if(!flag[v]) dfs(v); } } priority_queue<pair<ll, ll> > q; ll dis[N]; bool vis[N]; void dij() { for(ll i = 1; i <= n; i++) dis[i] = 1e15; dis[st] = 0; q.push(make_pair(0, st)); while(!q.empty()) { ll u = q.top().second; q.pop(); if(vis[u]) continue; vis[u] = 1; for(ll i = head[u]; i; i = nxt[i]) if(type[i] == 0) { ll v = to[i]; if(!go[v]) continue; if(!vis[v]) { if(dis[v] > dis[u] + 1) { dis[v] = dis[u] + 1; q.push(make_pair(-dis[v], v)); } } } } } int main() { scanf("%lld %lld", &n, &m); for(ll i = 1; i <= m; i++) { ll u, v; scanf("%lld %lld", &u, &v); addEdge(u, v, 0); addEdge(v, u, 1); } scanf("%lld %lld", &st, &ed); dfs(ed); for(ll u = 1; u <= n; u++) { go[u] = true; for(ll i = head[u]; i; i = nxt[i]) { ll v = to[i]; if(flag[v] == 0) go[u] = false; } } dij(); if(dis[ed] == 1e15) printf("-1"); else printf("%lld", dis[ed]); } /* 3 3 1 2 2 3 3 1 1 2 */
T2 联合权值
[NOIP2014 提高组] 联合权值
题目背景
NOIP2014 提高组 D1T2
题目描述
无向连通图 \(G\) 有 \(n\) 个点,\(n-1\) 条边。点从 \(1\) 到 \(n\) 依次编号,编号为 \(i\) 的点的权值为 \(W_i\),每条边的长度均为 \(1\)。图上两点 \((u, v)\) 的距离定义为 \(u\) 点到 \(v\) 点的最短距离。对于图 \(G\) 上的点对 \((u, v)\),若它们的距离为 \(2\),则它们之间会产生 \(W_v \times W_u\) 的联合权值。
请问图 \(G\) 上所有可产生联合权值的有序点对中,联合权值最大的是多少?所有联合权值之和是多少?
输入格式
第一行包含 \(1\) 个整数 \(n\)。
接下来 \(n-1\) 行,每行包含 \(2\) 个用空格隔开的正整数 \(u,v\),表示编号为 \(u\) 和编号为 \(v\) 的点之间有边相连。
最后 \(1\) 行,包含 \(n\) 个正整数,每两个正整数之间用一个空格隔开,其中第 \(i\) 个整数表示图 \(G\) 上编号为 \(i\) 的点的权值为 \(W_i\)。
输出格式
输出共 \(1\) 行,包含 \(2\) 个整数,之间用一个空格隔开,依次为图 \(G\) 上联合权值的最大值和所有联合权值之和。由于所有联合权值之和可能很大,输出它时要对 \(10007\) 取余。
样例 #1
样例输入 #1
5 1 2 2 3 3 4 4 5 1 5 2 3 10 样例输出 #1
20 74 提示
样例解释
本例输入的图如上所示,距离为 \(2\) 的有序点对有\((1,3)\) 、\((2,4)\) 、\((3,1)\) 、$(3,5) \(、\)(4,2)$ 、$(5,3) $。
其联合权值分别为 \(2,15,2,20,15,20\)。其中最大的是 \(20\),总和为 \(74\)。
数据说明
- 对于 \(30\%\) 的数据,\(1 < n \leq 100\);
- 对于 \(60\%\) 的数据,\(1 < n \leq 2000\);
- 对于 \(100\%\) 的数据,\(1 < n \leq 2\times 10^5\),\(0 < W_i \leq 10000\)。
保证一定存在可产生联合权值的有序点对。
样例太水了,脑子也抽了,忘记还有兄弟距离了。
题目说得很明白,就是一棵树。对于树上的每一个节点与其相连的两个不同节点距离为 2。
然后就是数学题了。
/** * @file 联合权值.cpp * @tag: #GMOJ#树 * @author: ZnPdCo * @date: 2023-12-30 14:26:53 * @problem: https://gmoj.net/senior/#main/show/3931 **/ #include <cstdio> #include <algorithm> #define ll long long #define N 200010 #define P 10007 using namespace std; ll n, w[N], ans1, ans2; ll fa[N]; ll dep[N]; ll f[N], mx[N]; ll head[N]; ll nxt[2*N]; ll to[2*N], cnt; void addEdge(ll u, ll v) { cnt++; to[cnt] = v; nxt[cnt] = head[u]; head[u] = cnt; } int main() { scanf("%lld", &n); for(ll i = 1; i < n; i++) { ll u, v; scanf("%lld %lld", &u, &v); addEdge(u, v); addEdge(v, u); } for(ll i = 1; i <= n; i++) { scanf("%lld", &w[i]); } for(ll u = 1; u <= n; u++) { ll sum1 = 0, sum2 = 0, mx1 = 0, mx2 = 0; for(ll j = head[u]; j; j = nxt[j]) { ll v = to[j]; (sum1 += w[v]) %= P; (sum2 += w[v]*w[v]) %= P; if(w[v] > mx1) mx1 = w[v]; else if(w[v] > mx2) mx2 = w[v]; } ans1 = max(ans1, mx1*mx2); (ans2 += sum1 * sum1 - sum2) %= P; } printf("%lld %lld", ans1, ans2); }
T3 飞扬的小鸟
[NOIP2014 提高组] 飞扬的小鸟
题目背景
NOIP2014 提高组 D1T3
题目描述
Flappy Bird 是一款风靡一时的休闲手机游戏。玩家需要不断控制点击手机屏幕的频率来调节小鸟的飞行高度,让小鸟顺利通过画面右方的管道缝隙。如果小鸟一不小心撞到了水管或者掉在地上的话,便宣告失败。
为了简化问题,我们对游戏规则进行了简化和改编:
游戏界面是一个长为 \(n\),高为 \(m\) 的二维平面,其中有 \(k\) 个管道(忽略管道的宽度)。
小鸟始终在游戏界面内移动。小鸟从游戏界面最左边任意整数高度位置出发,到达游戏界面最右边时,游戏完成。
小鸟每个单位时间沿横坐标方向右移的距离为 \(1\),竖直移动的距离由玩家控制。如果点击屏幕,小鸟就会上升一定高度 \(x\),每个单位时间可以点击多次,效果叠加;如果不点击屏幕,小鸟就会下降一定高度 \(y\)。小鸟位于横坐标方向不同位置时,上升的高度 \(x\) 和下降的高度 \(y\) 可能互不相同。
小鸟高度等于 \(0\) 或者小鸟碰到管道时,游戏失败。小鸟高度为 \(m\) 时,无法再上升。
现在,请你判断是否可以完成游戏。如果可以,输出最少点击屏幕数;否则,输出小鸟最多可以通过多少个管道缝隙。
输入格式
第 \(1\) 行有 \(3\) 个整数 \(n, m, k\),分别表示游戏界面的长度,高度和水管的数量,每两个整数之间用一个空格隔开;
接下来的 \(n\) 行,每行 \(2\) 个用一个空格隔开的整数 \(x\) 和 \(y\),依次表示在横坐标位置 \(0 \sim n-1\) 上玩家点击屏幕后,小鸟在下一位置上升的高度 \(x\),以及在这个位置上玩家不点击屏幕时,小鸟在下一位置下降的高度 \(y\)。
接下来 \(k\) 行,每行 \(3\) 个整数 \(p,l,h\),每两个整数之间用一个空格隔开。每行表示一个管道,其中 \(p\) 表示管道的横坐标,\(l\) 表示此管道缝隙的下边沿高度,\(h\) 表示管道缝隙上边沿的高度(输入数据保证 \(p\) 各不相同,但不保证按照大小顺序给出)。
输出格式
共两行。
第一行,包含一个整数,如果可以成功完成游戏,则输出 \(1\),否则输出 \(0\)。
第二行,包含一个整数,如果第一行为 \(1\),则输出成功完成游戏需要最少点击屏幕数,否则,输出小鸟最多可以通过多少个管道缝隙。
样例 #1
样例输入 #1
10 10 6 3 9 9 9 1 2 1 3 1 2 1 1 2 1 2 1 1 6 2 2 1 2 7 5 1 5 6 3 5 7 5 8 8 7 9 9 1 3 样例输出 #1
1 6 样例 #2
样例输入 #2
10 10 4 1 2 3 1 2 2 1 8 1 8 3 2 2 1 2 1 2 2 1 2 1 0 2 6 7 9 9 1 4 3 8 10 样例输出 #2
0 3 提示
【输入输出样例说明】
如下图所示,蓝色直线表示小鸟的飞行轨迹,红色直线表示管道。
【数据范围】
对于 \(30\%\) 的数据:\(5 \leq n \leq 10, 5 \leq m \leq 10, k=0\),保证存在一组最优解使得同一单位时间最多点击屏幕 \(3\) 次;
对于 \(50\%\) 的数据:\(5 \leq n \leq 20, 5 \leq m \leq 10\),保证存在一组最优解使得同一单位时间最多点击屏幕 \(3\) 次;
对于 \(70\%\) 的数据:\(5 \leq n \leq 1000, 5 \leq m \leq 100\);
对于 \(100\%\) 的数据:\(5 \leq n \leq 10000\),\(5 \leq m \leq 1000\),\(0 \leq k < n\),\(0 < x,y < m\),\(0 < p < n\),\(0 \leq l < h \leq m\), \(l + 1 < h\)。
赛时不知道为什么,直接上线段树,常数炸了。
其实可以直接用背包思想。原题就是一个可重复取的背包。
/** * @file 飞扬的小鸟.cpp * @tag: #GMOJ#dp * @author: ZnPdCo * @date: 2023-12-30 14:27:53 * @problem: https://gmoj.net/senior/#main/show/3932 **/ #include <cstdio> #include <cstring> #include <algorithm> #define ll long long #define N 10010 using namespace std; ll n, m, k, ans2; ll x[N], y[N]; ll f[2][N]; struct node { ll p, h, l; } a[N]; ll pos = 1; bool cmp(node x, node y) { return x.p < y.p; } int main() { scanf("%lld %lld %lld", &n, &m, &k); for(ll i = 1; i <= n; i++) { scanf("%lld %lld", &x[i], &y[i]); } for(ll i = 1; i <= k; i++) { scanf("%lld %lld %lld", &a[i].p, &a[i].l, &a[i].h); } sort(a+1, a+1+k, cmp); for(ll i = 1; i <= n; i++) { for(ll j = 0; j <= m; j++) f[i%2][j] = 1e15; for(ll j = x[i]+1; j < m; j++) { f[i%2][j] = min(f[i%2][j], min(f[(i-1)%2][j-x[i]], f[i%2][j-x[i]]) + 1); } for(ll j = m - x[i]; j <= m; j++) { f[i%2][m] = min(f[i%2][m], min(f[(i-1)%2][j], f[i%2][j]) + 1); } for(ll j = 0; j <= m - y[i]; j++) { f[i%2][j] = min(f[i%2][j], f[(i-1)%2][j+y[i]]); } // for(ll j = 0; j <= m; j++) printf("f[%lld][%lld]=%lld\n", i, j, f[i%2][j]); if(pos <= k && a[pos].p == i) { for(ll j = 0; j <= a[pos].l; j++) f[i%2][j] = 1e15; for(ll j = a[pos].h; j <= m; j++) f[i%2][j] = 1e15; for(ll j = 0; j <= m; j++) { if(f[i%2][j] != 1e15) { ans2 = max(ans2, pos); } } pos++; } } ll ans = 1e15; for(ll i = 1; i <= m; i++) { ans = min(ans, f[n%2][i]); } if(ans != 1e15) { printf("1\n"); printf("%lld", ans); } else { printf("0\n"); printf("%lld", ans2); } }
T4 解方程
[NOIP2014 提高组] 解方程
题目背景
NOIP2014 提高组 D2T3
题目描述
已知多项式方程:
\[a_0+a_1x+a_2x^2+\cdots+a_nx^n=0 \]求这个方程在 \([1,m]\) 内的整数解(\(n\) 和 \(m\) 均为正整数)。
输入格式
输入共 \(n + 2\) 行。
第一行包含 \(2\) 个整数 \(n, m\) ,每两个整数之间用一个空格隔开。
接下来的 \(n+1\) 行每行包含一个整数,依次为 \(a_0,a_1,a_2\ldots a_n\) 。
输出格式
第一行输出方程在 \([1,m]\) 内的整数解的个数。
接下来每行一个整数,按照从小到大的顺序依次输出方程在 \([1,m]\) 内的一个整数解。
样例 #1
样例输入 #1
2 10 1 -2 1 样例输出 #1
1 1 样例 #2
样例输入 #2
2 10 2 -3 1 样例输出 #2
2 1 2 样例 #3
样例输入 #3
2 10 1 3 2 样例输出 #3
0 提示
对于 \(30\%\) 的数据:\(0<n\le 2,|a_i|\le 100,a_n≠0,m<100\) 。
对于 \(50\%\) 的数据:\(0<n\le 100,|a_i|\le 10^{100},a_n≠0,m<100\) 。
对于 \(70\%\) 的数据:\(0<n\le 100,|a_i|\le 10^{10000},a_n≠0,m<10^4\) 。
对于 \(100\%\) 的数据:\(0<n\le 100,|a_i|\le 10^{10000},a_n≠0,m<10^6\) 。
赛时一眼 \(O(nm)\) 正解,然后不知道为什么我竟然每次都求一次 \(x^i\),常数送到 30pts。
因为 \(0\bmod p = 0\),我们可以利用模数的性质,把 \(a_i\) 都取模,然后就是正常暴力。
但是对于 \(p\) 的倍数怎么办呢?没关系,两个模数撞的概率比较小,就可以过了。
我只用了 \(998244353\)。
不要每次都求一次 \(x^i\)。
然后可以边输入边取模。
/** * @file 联合权值.cpp * @tag: #GMOJ#玄学 * @author: ZnPdCo * @date: 2023-12-30 14:27:22 * @problem: https://gmoj.net/senior/#main/show/3935 **/ #include <cstdio> #include <random> #include <ctime> #include <algorithm> using namespace std; #define ll long long #define N 110 #define P 998244353 mt19937 rnd(time(0)); ll n, m; ll a[N]; bool ismx; ll ans[N], cnt; ll read() { ll x=0, flag = 0; char c = '.'; while((c < '0' || c > '9') && c != '-') c = getchar(); if(c == '-') flag = 1, c = getchar(); while(c >= '0' && c <= '9') { x = ((x<<1)%P + (x<<3)%P + (c^'0')) % P; c = getchar(); } if(flag) x = (-x % P + P) % P; return x; } void output() { printf("%lld\n", cnt); for(ll i = 1; i <= cnt; i++) { printf("%lld\n", ans[i]); } } int main() { n = read(), m = read(); for(ll i = 0; i <= n; i++) { a[i] = read(); } for(ll i = 1; i <= m; i++) { ll sum = 0, xc = 1; for(ll j = 0; j <= n; j++) { (sum += a[j] * xc % P) %= P; (xc *= i) %= P; } if(sum == 0) ans[++cnt] = i; } output(); }
比赛赛时发挥不好,很多点都没有想到,特别是 T3,完全没有把背包联系起来(最后其实想到了,但不知道为什么自己丢掉了)
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现