A. 小L的三角尺
本题主要内容:小
有
把直角三角尺,每把尺子的两条直角边分别为
(不可打磨)和
(可打磨)。
他可以选择一个非负整数
打磨第
把尺子的
边,使得打磨后长度变为
−
,且 0≤
≤
。
所有尺子的打磨长度之和
不能超过总打磨额度
。
打磨后,每把尺子的斜边长度为
(当
=
时,斜边长度为
)。
现在需要在总打磨额度限制下,找到最优的打磨策略,使得所有尺子的斜边长度之和最小。
本题大致思路:使得所有尺子的斜边长度之和最小,就是把所有斜边求出总和再 减去 因为打磨所损耗的斜边的长度。
由于无法得知每个直角边怎么打磨才是最优的打磨方式,并且
并不是及其庞大,所以我们可以每次将打磨长度为 1 所损耗的斜边长度最大的那个三角形找到。
并重复上述操作,直至直角边无法继续打磨 或者 将要超过总打磨额度
。
由于以上的找最大值的特性,我们考虑可以用优先队列的方式,并且每个三角形所存储的东西有点多,所以可以考虑用结构体来储存三角形的性质。
其中包括:打磨长度为 1 的直角边导致斜边所损耗的值,两个直角边,以及这个三角形已经被打磨了多少长度了。
最后按照 打磨长度为 1 的直角边导致斜边所损耗的值 进行排序,即可得出结果。
代码如下:
#include <iostream>
#include <algorithm>
#include <cmath>
#include <queue>
using namespace std;
typedef long long LL;
const int N = 5e5 + 100;
LL n, w;
struct S
{
double fu; //打磨长度为 1 的直角边导致斜边所损耗的值
double x; //直角边x
double y; //直角边y
double now; //这个三角形已经被打磨了的长度
bool operator<(const S& other) const //使得优先队列为大根堆,用于排序
{
return fu < other.fu;
}
};
int main()
{
scanf("%lld %lld", &n, &w);
double all = 0; // 所有尺子初始斜边长度之和
priority_queue<S> q; // 优先队列,按磨损长度降序排列
for(int i = 1; i <= n; i++)
{
double x, y;
scanf("%lf %lf", &x, &y);
all += sqrt(x * x + y * y); //计算初始斜边长度
double fu = sqrt(x * x + y * y) - sqrt(x * x + (y-1) * (y-1)); //计算当前尺子斜边损耗长度
q.push({fu, x, y, 0}); //当前尺子的初始状态入队:磨损长度 fu,已打磨量 now = 0
}
int use = 0; // 已使用的打磨额度
double res = 0; // 已经减少的总长度
while(use < w && !q.empty() )
{
auto t = q.top(); // 取出当前收益最大的尺子
q.pop();
if(t.fu <= 0) break; //如果收益 <= 0,则继续打磨不会减少总长,提前结束
// 打磨这一尺子
t.now += 1; // 已打磨量加1
res += t.fu; // 累加减少的长度
use++; // 使用额度加1
if(t.y > t.now)
{
// 当前打磨 t.now 单位后 再打磨1单位所损耗的斜边长
double f = sqrt(t.x * t.x + (t.y-t.now) * (t.y-t.now))
- sqrt(t.x * t.x+(t.y - t.now - 1) * (t.y - t.now - 1));
q.push({f, t.x, t.y, t.now}); //处理完后继续入队
}
}
printf("%.12lf\n", all - res);
return 0;
}
B. 小L的彩球
本题主要内容:小 L 把编号为 1 到
的
个彩球分到左右两个盒子里,左盒放
个、右盒放
个,
相邻编号的彩球之间有连线,若两球分属不同盒子则连线外露;
已知有 t 条连线外露,需要计算满足条件的彩球放置方法数,结果对 998244353 取模,每组输出对应答案。
本题主要内容:我们假设 如果连线外露,就相当于分成两段,那么总共就会分成
段。
如果 以左侧盒子开头,那么左侧盒子有的段数
应该是
,右侧盒子有的段数
应该是
接下来就是: 将
个 左侧盒子的球分配到
段中,每段至少1个,方案数为
(隔板法,每段至少 1 个)
将
个 右侧盒子的球分配到
段中,每段至少1个,方案数为
(隔板法,每段至少 1 个)
最后两者相乘就是答案。
代码实现:
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 1e6 + 100;
const LL MOD = 998244353;
LL T, n, x, t;
LL fact[N], infact[N]; // fact[i] 存储 i! % MOD, infact[i] 存储 i! 的逆元 % MOD
// 快速幂
LL qmi(LL a, LL b, LL p)
{
LL res = 1;
while (b)
{
if (b & 1) res = res * a % p;
a = a * a % p;
b >>= 1;
}
return res;
}
// 组合数 C(a, b) % p
LL f(LL a, LL b, LL p)
{
return fact[a] * infact[b] % p * infact[a - b] % p;
}
int main()
{
scanf("%lld", &T);
// 预处理阶乘和阶乘逆元
fact[0] = infact[0] = 1;
for (int i = 1; i < N; i++)
{
fact[i] = fact[i - 1] * i % MOD;
infact[i] = infact[i - 1] * qmi(i, MOD - 2, MOD) % MOD; // i! 的逆元 = (i-1)! 的逆元 * i 的逆元
}
while (T--)
{
scanf("%lld %lld %lld", &n, &x, &t);
// 特判 t = 0 的情况:此时所有球在同一盒子,要么全左要么全右
if (t == 0)
{
// 左边盒子有 x 个,右边有 n-x 个;当 x == n 时全左,x == 0 时全右
if (x == n || x == 0)
printf("1\n");
else
printf("0\n");
continue;
}
// 露在外面的线数为 t,则段数 k = t + 1
int k = t + 1;
// 两种可能的颜色排列:
// 1. 开头在左侧
// 2. 开头在右侧
int l1 = (k + 1) / 2; // 第一种情况中 L 段数
int r1 = k / 2; // 第一种情况中 R 段数
int l2 = k / 2; // 第二种情况中 L 段数
int r2 = (k + 1) / 2; // 第二种情况中 R 段数
LL res = 0;
// 第一种情况:以 L 开头,需要 x >= l1 且 n-x >= r1 才能每段至少一个球
if (x >= l1 && n - x >= r1)
res = (res + f(x - 1, l1 - 1, MOD) * f(n - x - 1, r1 - 1, MOD)) % MOD;
// 第二种情况:以 R 开头,需要 x >= l2 且 n-x >= r2
if (x >= l2 && n - x >= r2)
res = (res + f(x - 1, l2 - 1, MOD) * f(n - x - 1, r2 - 1, MOD)) % MOD;
printf("%lld\n", res);
}
return 0;
}
D. 小L的扩展
本题主要内容:在一张
行
列的方格纸上,初始时有
个黑格和
个蓝格,黑格会在每个时间单位向四个方向扩散一格,
但蓝格在其变为白色的时刻
之前无法被黑格扩散覆盖;每个时刻
的处理顺序是:
先让所有
的蓝格变白,再让黑格扩散,若扩散到的白格则会被染黑;需要求出整张纸所有方格都被染黑的时间。
本题主要思路:我首先想到的就是多源BFS,将黑色的格子都入队,然后找其他格子,每个时间单位会向外 扩展距离为1的其他格子。
但是由于有蓝色的格子,由于蓝色格子可能引入等待,导致扩散不是按层同步进行,普通BFS(队列)无法保证每个格子第一次被访问就是最早时间。
使用最小堆保证每次取出当前时间最小的格子,从而像Dijkstra一样正确更新。
黑色每单位时间向四邻域扩散一格。如果邻格是白色的,直接 当前时间+1。
如果邻格是蓝色,必须等到它变白后才能被染黑,因此到达时间为 max(当前时间+1, 蓝色变白时间)。
如果邻格是蓝色,必须等到它变白后才能被染黑,因此到达时间为 max(当前时间+1, 蓝色变白时间)。
代码实现:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
typedef pair<long long, long long> PLL;
typedef long long LL;
const int N = 1e6 + 100;
int n, m, a, b;
LL g[N]; // 存储每个格子的初始状态:0=初始黑,正数=蓝色及其变白时间,-1=普通白
LL dist[N]; // 记录每个格子被染黑的最早时间,-1表示尚未被染黑(或未更新)
priority_queue<PLL, vector<PLL>, greater<PLL>> q; // 小根堆,按时间从小到大处理
void bfs()
{
// 四个方向的位移:上(-m)、下(m)、左(-1)、右(1) (注意一维存储时,列数为 m)
int dx[] = {-m, m, -1, 1};
memset(dist, -1, sizeof(dist)); // 初始化距离为 -1
// 将所有初始黑格(g[i]==0)入队,时间为 0
for (int i = 1; i <= m * n; i++)
{
if (g[i] == 0)
{
dist[i] = 0;
q.push({0, i});
}
}
while (!q.empty())
{
auto t = q.top(); // 取出当前时间最小的格子
q.pop();
LL curTime = t.first;
LL curPos = t.second;
// 将一维坐标转换为行列,用于边界判断
int row = (curPos - 1) / m + 1;
int col = (curPos - 1) % m + 1;
// 尝试向四个方向扩散
for (int i = 0; i < 4; i++)
{
// 边界检查:防止越界
if (col == 1 && dx[i] == -1) continue; // 最左列不能向左
if (col == m && dx[i] == 1) continue; // 最右列不能向右
if (row == 1 && dx[i] == -m) continue; // 第一行不能向上
if (row == n && dx[i] == m) continue; // 最后一行不能向下
int nxtPos = curPos + dx[i]; // 邻居的一维坐标
LL nxtTime; // 邻居可能被染黑的时间
// 根据邻居的类型计算其被染黑的时间
if (g[nxtPos] == -1) // 普通白色格子
nxtTime = curTime + 1;
else if (g[nxtPos] > 0) // 蓝色格子:需要等它变白
nxtTime = max(curTime + 1, g[nxtPos]); // 取扩散时间和变白时间的较大者
else // g[nxtPos]==0 是初始黑格,不会作为未染黑格子被更新,直接跳过
continue;
// 如果邻居尚未被染黑,或者找到了更早的时间,则更新并加入队列
if (dist[nxtPos] == -1 || nxtTime < dist[nxtPos])
{
dist[nxtPos] = nxtTime;
q.push({nxtTime, nxtPos});
}
}
}
}
int main()
{
scanf("%d %d %d %d", &n, &m, &a, &b);
// 初始化所有格子为普通白色(-1)
memset(g, -1, sizeof(g));
// 读入 a 个初始黑格,标记为 0
for (int i = 1; i <= a; i++)
{
int x, y;
scanf("%d %d", &x, &y);
g[(x - 1) * m + y] = 0; // 将二维坐标映射到一维
}
// 读入 b 个蓝色格子,标记为它们的变白时间 t
for (int i = 1; i <= b; i++)
{
LL x, y, t;
scanf("%lld %lld %lld", &x, &y, &t);
g[(x - 1) * m + y] = t; // 存储变白时间
}
bfs(); // 执行扩散计算
// 找出所有格子被染黑时间的最大值,即整个网格完全变黑的时间
LL ma = 0;
for (int i = 1; i <= n * m; i++)
if (dist[i] != -1)
ma = max(ma, dist[i]);
printf("%lld\n", ma);
return 0;
}
E. 小L的空投
本题主要内容:A 国共有
个城市和
条双向公路,每个城市有一个高度
。在接下来
天里,每天水位会上升到
(严格递增),
所有高度 ≤
的城市会被淹没,与其直接相连的公路也不可行。
对于每一天,需要统计所有未被淹没的城市中,连通分量大小 ≥
的数量,这个数量就是当天需要发放的空投总数。
本题主要思路:使用倒序处理对于当前水位
,我们需要将所有高度大于
的城市加入到连通图中。
由于我们是从高水位向低水位处理,所以每次要加入的城市就是那些高度比当前水位更高的城市。
每次加入完成后,就维护连通块。当加入城市 u 时,首先检查是否是独立的连通块,或者是否与其他连通块中的某点相邻,进行合并。
代码实现:
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int,int> PII;
const int N = 5e5 + 100;
int n,m,x,d;
int h[N]; // 每个城市的高度
int p[N], s[N]; // 并查集父节点和集合大小
int cnt = 0; // 当前满足大小≥d的连通块个数
vector<int> adj[N]; // 邻接表
vector<int> H; // 每天的水位
int find(int x)
{
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 合并两个城市所在的集合,并更新cnt
void unite(int a, int b)
{
int ra = find(a), rb = find(b);
if(ra == rb) return;
//更新cnt:减去原来两个可能有的贡献
if(s[ra] >= d) cnt--;
if(s[rb] >= d) cnt--;
// 按大小合并,保证ra是较大的集合
if(s[ra] < s[rb]) swap(ra, rb);
p[rb] = ra;
s[ra] += s[rb];
// 更新cnt:加上新集合可能的贡献
if(s[ra] >= d) cnt++;
}
int main()
{
scanf("%d %d %d %d", &n, &m, &x, &d);
for(int i = 1; i <= n; i++) scanf("%d", &h[i]);
// 建图
for(int i = 0; i < m; i++)
{
int u, v;
scanf("%d %d", &u, &v);
adj[u].push_back(v);
adj[v].push_back(u);
}
for(int i = 0; i < x; i++)
{
int tmp;
scanf("%d", &tmp);
H.push_back(tmp);
}
// 将城市按高度从大到小排序加
vector<PII> c; // (高度, 城市编号)
for(int i = 1; i <= n; i++)
c.push_back(h[i], i);
sort(c.begin(), c.end(), greater<PII>()); // 降序
int idx = 0; // 指向c中下一个待加入的城市
int a[x + 1]; // 存储每天答案(下标0对应第1天)
// 倒序遍历每一天
for(int i = x - 1; i >= 0; i--)
{
int now = H[i]; // 当前天的水位
// 将所有高度 > now 的城市加入并查集(这些城市在这一天未被淹没)
while(idx < n && c[idx].first > now)
{
int u = c[idx].second; // 城市编号
p[u] = u; // 初始化并查集
s[u] = 1;
if(d <= 1) cnt++; // 如果d=1,每个单独城市就是一个合法连通块
// 尝试与已经加入的邻居合并
for(int v : adj[u])
{
if(p[v] != 0) // 邻居v已经被加入
unite(u, v);
}
idx++;
}
a[i] = cnt; // 这一天的答案
}
for(int i = 0; i < x; i++)
printf("%d\n", a[i]);
return 0;
}
G. 小L的散步
本题主要内容:小
面前有
个石块,每个石块长度为
,从位置 0 开始依次排列,相邻石块间有缝隙(最后一个石块的右端点也视为缝隙)。
他从位置 0 出发,每一步跨过长度
,脚掌长度为
,脚后跟位置即为当前位置。
需要判断:在初始位置以及每一步走完后的位置,他的脚掌是否覆盖了任何一个缝隙(脚掌边缘恰好落在缝隙上不算踩中)。
他从位置 0 出发,每一步跨过长度
需要判断:在初始位置以及每一步走完后的位置,他的脚掌是否覆盖了任何一个缝隙(脚掌边缘恰好落在缝隙上不算踩中)。
如果有,输出 YES,否则输出 NO。
本题大致思路:我么可以发现,对于石块的长度的前缀和,就是每个缝隙所在的位置。因此我们可以先计算
的前缀和,
然后依次遍历每走一步,计算脚后跟
和脚尖
,判断是否存在某一缝隙满足:
如果存在就是YES,否则就是NO。
代码实现:
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 2e5 + 100;
LL n, m, r;
LL s[N], y[N]; // s[i] 为前 i 个石块的总长度(即第 i 个石块的右端点位置),y[i] 为第 i 步步长
int main()
{
scanf("%lld %lld %lld", &n, &m, &r);
// 读入每个石块长度并计算前缀和
for (int i = 1; i <= n; i++)
{
scanf("%lld", &s[i]);
s[i] += s[i - 1];
}
for (int i = 1; i <= m; i++)
scanf("%lld", &y[i]);
LL l = 0; // 当前脚后跟的位置,初始为 0
LL cnt = 1; // 指向当前可能踩到的第一个缝隙(即第 cnt 个石块的右端点)
// 检查初始位置是否踩中缝隙
if (s[cnt] > l && s[cnt] < r) // 缝隙位于脚掌区间 (l, r) 内
{
printf("YES\n");
return 0;
}
for (int i = 1; i <= m; i++)
{
l += y[i]; // 脚后跟向前移动 y[i]
r += y[i]; // 脚掌右端点也相应移动(脚长不变,所以 r 是脚后跟 + 脚长)
// 当脚后跟已经超过当前缝隙位置时,说明这个缝隙已经被跨过,需要检查下一个缝隙
while (cnt <= n && l > s[cnt])
cnt++;
// 如果当前缝隙在脚掌区间内,则踩中
if (cnt <= n && s[cnt] > l && s[cnt] < r)
{
printf("YES\n");
return 0;
}
}
// 全程没有踩中缝隙
printf("NO\n");
return 0;
}
H. 小L的数组
规则一:将
规则二:将
要求计算在执行完所有
本题主要思路:由于数据范围并不大,所以我们可以把每次按不同规则所产生的数都记录下来,然后分别将这些数进行下一次的两种操作。
由于考虑到去重的问题,我们可以选择用 unordered_set 来记录。
代码实现:
#include <iostream>
#include <algorithm>
#include <unordered_set>
using namespace std;
const int N = 3000;
int n;
int a[N], b[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%d", &a[i]);
for (int i = 0; i < n; i++)
scanf("%d", &b[i]);
// 使用哈希集合存储当前所有可能的 x 值
unordered_set<int> s;
s.insert(0); // 初始 x = 0
// 依次进行 n 次操作
for (int i = 0; i < n; i++)
{
unordered_set<int> tmp; // 临时集合,存放第 i 步后的所有可能值
for (auto x : s)
{
// 规则一:减去 a[i] 并保证非负
tmp.insert(max(0, x - a[i]));
// 规则二:与 b[i] 按位异或
tmp.insert(x ^ b[i]);
}
// 将当前集合更新为临时集合
s.swap(tmp);
}
// 找出所有可能结果中的最大值
int res = -1e9;
for (auto i : s)
res = max(res, i);
printf("%d\n", res);
return 0;
}
K. 小L的游戏1
本题主要内容:游戏在初始值为 0 的变量
上进行,小
和 falleaves01 轮流对
累加操作,小
先手。
每一轮分为两个回合:
小
的回合:
falleaves01 的回合:
每一轮分为两个回合:
小
falleaves01 的回合:
一旦
,游戏立即结束。需要判断最后一次操作是谁执行的.
本题主要思路:经过两个回合,
增加的值就是
,因此我们可以先让最终的
对
取模
如果最后的余数是 0 ,就说明最后一次是 fallleaves01 操作的,输出1
如果最后的余数 大于0 小于等于
,就说明最后一次是 小
操作的,输出0
如果最后的余数是 大于
小于
,就说明最后一次是 fallleaves01 操作的,输出1
代码实现:
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
LL t;
LL m, n, z;
int main()
{
scanf("%lld", &t);
while (t--)
{
scanf("%lld %lld %lld", &m, &n, &z);
// 每一轮两人共增加 m+n,所以用 z 对 (m+n) 取模得到最后一轮开始后还需增加的量
LL res = z % (m + n);
if (res == 0)
// 余数为 0:经过整数轮后恰好达到 z,最后一轮的最后一次操作是对方加的 n
printf("1");
else if (res <= m)
// 余数在 [1, m] 之间:最后一轮中小 L 加 m 后直接达到或超过 z,因此最后一次是小 L
printf("0");
else
// 余数大于 m:小 L 加 m 后仍未达到,对方加 n 后才达到,最后一次是对方
printf("1");
}
return 0;
}

京公网安备 11010502036488号