2026.8.8暑假作业第二套——做题随笔题解
2026.8.8暑假作业第二套——做题随笔/题解
文章目录
做题感受
这是一套较为简单但需要一点点思考且不那么常规的题。题目叙述乍一看“欸!这题我会做!”实际上暗含了一部分“坑”。
T1苹果树
题意简述
给你一个长度为 n n n的序列 a a a(3≤ n n n≤1000)。其中有 n − 1 n-1 n−1个元素都相同,让你找出剩下一个不同元素的位置和键值。
赛时感受
这么简单的签到题?切了。
T2机器人寻宝
题意简述
在平面坐标系内给你 n n n个点的坐标 ( x i , y i ) (x_i,y_i) (xi,yi)(0≤xi,yi≤1e9;xi,yi均为整数),你可以定义若干个移动模式:
每个移动模式包含两个参数 a , b a,b a,b,意味着机器人可以从坐标 ( x , y ) (x,y) (x,y)移动到 ( x + a , y + b ) (x+a,y+b) (x+a,y+b);
每个移动模式可以被调用任意正整数次;
现在想知道:至少定义多少个移动模式,才能实现任意两点之间能通过若干次调用同一移动模式到达?
赛时感受
很快想到题目实际上可以转化成:统计平面内直线条数,最后输出答案 × 2 ×2 ×2即可。1
可是要如何记录已经出现过的直线呢?要使用 y = k x + b y=kx+b y=kx+b double的精度误差2能害死我。
嗯…不会,敲个暴力走了🛺🛺🛺
正解
思路
确实不能直接存斜率截距,但是我们可以存两点之间的有向差量 (好吧我之前没听说过这玩意儿)。也就是说:
对于任意两点 P i , P j P_i,P_j Pi,Pj,计算有向差量:
( d x , d y ) = ( x j − x i , y j − y i ) (dx,dy)=(x_j-x_i,y_j-y_i) (dx,dy)=(xj−xi,yj−yi)
一条移动模式 ( a , b ) (a,b) (a,b)能从 P i P_i Pi一次到 P j P_j Pj,当且仅当存在正整数 k k k,使得:
( d x , d y ) = k ⋅ ( a , b ) (dx,dy)=k·(a,b) (dx,dy)=k⋅(a,b)
所以只需要把每个差向量化简成最简整数方向:
( d x g , d y g ) , g = g c d ( ∣ d x ∣ , ∣ d y ∣ ) (\frac{dx}{g},\frac{dy}{g}),g=gcd(|dx|,|dy|) (gdx,gdy),g=gcd(∣dx∣,∣dy∣)
并且它的相反方向也要加入,因为反过来走需要另一种模式。
最后统计不同的最简有向向量个数即可。
实现步骤
- 读入所有点。
- 枚举所有点对 i < j i<j i<j。
- 计算 d x , d y dx,dy dx,dy。
- 求出 g = g c d ( d x , d y ) g=gcd(dx,dy) g=gcd(dx,dy),得到最简方向。
- 把这个方向和它的相反方向都存入数组。
- 排序、去重,输出数组大小。
参考代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int o=5e2+22; // 最大点数 500,数组大小略大
int n;
// 存储每个宝藏的坐标
struct node
{
int x,y;
}a[o];
// 手动实现最大公约数(欧几里得算法)
// 先取绝对值,处理负数情况,保证 gcd 非负
int gcdll(int a,int b)
{
if(a<0) a=-a;
if(b<0) b=-b;
while(b)
{
int t=a%b;
a=b;
b=t;
}
return a;
}
signed main()
{
//freopen("name.in", "r", stdin);
//freopen("name.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i].x>>a[i].y;
}
// 存储所有不同的有向最简向量(方向模式)
vector<pair<int,int>> ans;
// 枚举所有点对 (i, j),i < j
for(int i=1;i<=n;i++)
{
for(int j=i+1;j<=n;j++)
{
// 计算从 j 到 i 的差向量(也可以反过来,不影响,因为会同时加入相反方向)
int dx=a[i].x-a[j].x;
int dy=a[i].y-a[j].y;
// 排除相同点(数据保证不会出现,但保留安全)
if(dx==0&&dy==0) continue;
// 化简为最简方向向量
int f=gcdll(dx,dy);
ans.push_back({dx/f, dy/f}); // 原方向
ans.push_back({-dx/f, -dy/f}); // 相反方向,因为从 j 到 i 也需要模式
}
}
// 排序去重,统计不同模式的数量
sort(ans.begin(),ans.end());
ans.erase(unique(ans.begin(),ans.end()),ans.end());
// 输出答案
cout<<ans.size()<<"\n";
return 0;
}
/*
样例测试:
3
1 2
3 6
7 4
输出 6
3
1 1
2 2
3 3
输出 2
3
1 1
2 1
0 1
输出 2
*/
小结
这道题算是一道还可以的“数论 + 暴力枚举 + 去重”类题目,主要就是有向差量+GCD要拐一下,其他还好。
调试心得(血泪汇总)
- 千万不要用 d o u b l e double double存斜率截距来做,虽然我没试过能不能卡过,但这样的习惯是不好滴
- 一条直线,两种移动模式,不要漏存
- 而且,也不能只存单向去重后最后输出结果 × 2 ×2 ×2,这样做会导致答案偏大,因为单向集合里可能已经同时包含了某个方向和它的相反方向,直接乘以 2 会把它们重复计算。具体见参考代码后的第三个样例
只要不犯贱,其他都还好…
T3大富翁
题意简述
现在需要将 1 1 1通过一系列操作提升到 E E E( 1 ≤ E ≤ 1 e 18 1≤E≤1e18 1≤E≤1e18),给予你两种操作:
- 将当前的数加上1~ k k k( 1 ≤ k ≤ 1000 1≤k≤1000 1≤k≤1000)中任意一个数;
- 将当前的数乘 p p p( 1 ≤ p ≤ 1000 1≤p≤1000 1≤p≤1000);
问最少操作次数。
赛时感受
难得不考数据结构,而且这题目叙述比我脸都干净。从1到 E E E,到约数然后加倍?不行, p p p改不了。在所有约数中枚举选一个来将就 p p p?可 E E E有 1 e 18 1e18 1e18啊,这个范围肯定不是拿来给我枚举的。啊哈,正难则反!从 E E E降到 1 1 1完全等价嘛!接下来——能除就除,不行就减到下一个能除的数。嗯哏,AC。
贪心正确性
1. 问题重新建模(反向视角)
正向:从 1 出发,操作是 +1~+k 或 *p(其中 k < p,这是题目给定的关键条件)。
反向:从 E 出发,操作是 -1~-k 或 ÷p(必须能整除时才能用)。
我们定义函数 f(x) 表示从当前数 x 变到 1 的最少步数。
显然,当 x < p 时,无法使用除法,只能用减法:f(x) = ceil((x - 1) / k)
当 x ≥ p 时,为了使用除法,我们必须先通过减法将 x 变成 p 的倍数。
设 x = q × p + r,其中 0 ≤ r < p。
贪心策略:只减到最近的倍数 q × p,即只减 r(如果 r=0 就直接除)。
那么贪心代价为:
cost_greedy = ceil(r / k) + 1 + f(q)
2. 为什么不考虑“减到更远的倍数”(例如 (q - t) × p)?
假设我们不只减 r,而是多减 t × p(t ≥ 1),也就是减到 (q - t) × p。
那么代价为:
cost_t = ceil((r + t × p) / k) + 1 + f(q - t)
我们要证明:cost_greedy 永远小于或等于 cost_t。
关键不等式(核心):
因为题目给定 p > k(这是保证贪心成立的唯一数学条件),所以:
ceil((r + t × p) / k) ≥ ceil(r / k) + t
说明:多减 t × p 这一步,至少要额外付出 t 次减法操作(实际上因为 p > k,额外付出的次数还大于 t)。
另一方面,减到更小的商 q - t 后,最多能省下多少步呢?
最理想的情况是:q - t 比 q 小很多,从而省下一些除法/减法次数。但即便在最乐观的情况下,从 q 变到 q - t 也只需要最多 t 次操作(比如全用减法),所以:
f(q - t) ≥ f(q) - t
也就是说,把数变小 t 个单位,最多只能节省 t 步3。
合并:
把两个不等式相加:
cost_t ≥ [ceil(r/k) + t] + 1 + [f(q) - t]
= ceil(r/k) + 1 + f(q)
= cost_greedy
结论:多减 t × p 带来的额外减法开销(至少 t 步),完全抵消甚至超过它能节省的步骤(最多 t 步)。因此,减到比“最近倍数”更小的倍数,绝不会更优。
所以,每一步“只减余数 r”就是全局最优的贪心策略。
参考代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int o = 1e5 + 22;
// 全局变量
int k, p, e;
// k : 指定骰子一次最多可走的步数
// p : 倍数骰子的乘数
// e : 终点
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
// 读入三个整数
cin >> k >> p >> e;
int ans = 0; // 记录“混合使用两种骰子”的最少步数
int x = e - 1; // 只用指定骰子时,从 1 到 e 需要前进的总步数
// 反向递推:从终点 e 倒推回起点 1
// 循环条件:当前值 e 不小于倍数乘数 p,这样才可能使用倍数骰子
while (e >= p)
{
// 特殊处理 p == 1 的情况,因为除以 1 和取模 1 会导致死循环
if (p == 1)
{
ans = LLONG_MAX; // 置为无穷大,表示该方案无效
break;
}
// 计算当前值 e 除以 p 的余数 r
int ned = e % p;
// 需要先使用指定骰子将 e 减到最近的 p 的倍数(即 e - ned)
// 每次指定骰子最多走 k 步,因此所需次数为 ceil(ned / k)
ans += ned / k;
if (ned % k) ans++; // 处理向上取整
// 执行减法:将 e 变为 p 的倍数
e -= ned;
// 使用一次倍数骰子:从 e 跳到 e / p
ans++;
e /= p;
}
// 循环结束后,e 已经小于 p,无法再使用倍数骰子
// 现在只能用指定骰子从当前位置 e 走到 1(起点)
// 需要前进 (e - 1) 步,所需次数为 ceil((e - 1) / k)
if (e != 1 && p != 1) // 当 p==1 时已经置为无穷大,不再计算
{
ans += (e - 1) / k;
if ((e - 1) % k) ans++;
}
// 计算“只用指定骰子”的方案所需次数
// 从 1 到 e 需要前进 (e - 1) 步,每次最多走 k 步
int num = x / k;
if (x % k) num++; // 向上取整
// 答案取两种方案的最小值
cout << min(ans, num) << "\n";
return 0;
}
T4背包问题
题意简述
有一个背包的最大载重为 H H H,容积为 V V V( 1 ≤ H , V ≤ 300 1≤H,V≤300 1≤H,V≤300)。这个背包可以按 1 : 1 1:1 1:1的汇率将载重换成容积(只能单向转换)。现有 n n n( 1 ≤ n ≤ 1000 1≤n≤1000 1≤n≤1000)个物品,每物品 i i i有三个属性: h i h_i hi重量、 V i V_i Vi体积、 W i W_i Wi价值。
求能取得的最大价值。
赛时感受
害,把体积和重量一加不完事儿了吗?Σ(⊙▽⊙"a 不对!单向转化,Oh no!不会,敲个暴力走了🛺🛺🛺
正解
解题思路
1. 转化约束条件
若选择某个 k k k,则所选物品集合 S S S 必须满足:
∑ i ∈ S w i ≤ W − k , ∑ i ∈ S v i ≤ V + k \sum_{i\in S} w_i \le W - k,\quad \sum_{i\in S} v_i \le V + k i∈S∑wi≤W−k,i∈S∑vi≤V+k
将两式相加,可得:
∑ i ∈ S ( w i + v i ) ≤ W + V \sum_{i\in S} (w_i + v_i) \le W + V i∈S∑(wi+vi)≤W+V
同时,重量约束依然存在:
∑ w i ≤ W − k ≤ W \sum w_i \le W - k \le W ∑wi≤W−k≤W
(因为 k ≥ 0 k\ge0 k≥0,所以 ∑ w i ≤ W \sum w_i \le W ∑wi≤W 是必要条件,且若该条件满足,总能通过选择适当的 k k k 使第一个不等式成立,因为 k k k 可以取足够大?实际上,更严谨的转化是:存在 k ≥ 0 k\ge0 k≥0 使得:
∑ w i ≤ W − k ⟹ k ≤ W − ∑ w i \sum w_i \le W - k \implies k \le W - \sum w_i ∑wi≤W−k⟹k≤W−∑wi
且:
∑ v i ≤ V + k ⟹ k ≥ ∑ v i − V \sum v_i \le V + k \implies k \ge \sum v_i - V ∑vi≤V+k⟹k≥∑vi−V
因此存在这样的 k k k 当且仅当:
∑ v i − V ≤ W − ∑ w i ⟺ ∑ ( w i + v i ) ≤ W + V \sum v_i - V \le W - \sum w_i \iff \sum (w_i+v_i) \le W+V ∑vi−V≤W−∑wi⟺∑(wi+vi)≤W+V
同时还需 ∑ w i ≤ W \sum w_i \le W ∑wi≤W(因为 k ≥ 0 k\ge0 k≥0,从 k ≤ W − ∑ w i k \le W - \sum w_i k≤W−∑wi 可知需 ∑ w i ≤ W \sum w_i \le W ∑wi≤W)。
所以,可行性等价于:
∑ w i ≤ W , ∑ ( w i + v i ) ≤ W + V \sum w_i \le W,\quad \sum (w_i + v_i) \le W+V ∑wi≤W,∑(wi+vi)≤W+V
2. 动态规划
我们有两个限制维度:总重量(限制 W W W)和总消耗 s = w + v s = w+v s=w+v(限制 W + V W+V W+V)。
定义 d p [ j ] [ s ] dp[j][s] dp[j][s] 表示考虑若干物品后,总重量恰好为 j j j,总消耗恰好为 s s s 时能获得的最大价值。
转移为 0/1 背包:
d p [ j ] [ s ] = max ( d p [ j ] [ s ] , d p [ j − w i ] [ s − ( w i + v i ) ] + v a l i ) dp[j][s] = \max(dp[j][s],\; dp[j-w_i][s-(w_i+v_i)] + val_i) dp[j][s]=max(dp[j][s],dp[j−wi][s−(wi+vi)]+vali)
其中 j j j 从 W W W 降到 w i w_i wi, s s s 从 W + V W+V W+V 降到 w i + v i w_i+v_i wi+vi。
初始化所有 d p dp dp 为负无穷或 0(因为价值非负,用 0 可保证不选任何物品时价值为 0)。
最终答案为:
max 0 ≤ j ≤ W , 0 ≤ s ≤ W + V d p [ j ] [ s ] \max_{0\le j\le W,\;0\le s\le W+V} dp[j][s] 0≤j≤W,0≤s≤W+Vmaxdp[j][s]
3. 复杂度
- 时间复杂度: O ( n ⋅ W ⋅ ( W + V ) ) O(n \cdot W \cdot (W+V)) O(n⋅W⋅(W+V)),在数据范围内可承受。
- 空间复杂度: O ( W ⋅ ( W + V ) ) O(W \cdot (W+V)) O(W⋅(W+V)),可用二维数组或
vector动态分配。
参考代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int o=1e3+22;
int n,w,v;
struct node
{
int w,v,val;
}a[o];
int dp[322][622]={0}; // dp[重量][总消耗]:当前总重量为i,总消耗(重量+体积)为j时的最大价值
signed main()
{
//freopen("name.in", "r", stdin);
//freopen("name.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
// 输入物品数量n、背包原始重量上限w、体积上限v
cin>>n>>w>>v;
for(int i=1;i<=n;i++)
{
cin>>a[i].w>>a[i].v>>a[i].val;
}
// 0/1背包DP,枚举每个物品
for(int k=1;k<=n;k++)
{
int k_w=a[k].w; // 当前物品重量
int k_val=a[k].val; // 当前物品价值
int k_wv=a[k].w+a[k].v; // 当前物品的“总消耗” = 重量+体积
// 倒序枚举重量(第一维)和总消耗(第二维),保证每个物品只选一次
for(int i=w;i>=k_w;i--)
{
for(int j=w+v;j>=k_wv;j--)
{
// 状态转移:选或不选当前物品
dp[i][j]=max(dp[i][j],dp[i-k_w][j-k_wv]+k_val);
}
}
}
// 统计所有满足总重量<=w且总消耗<=w+v的状态的最大价值
int ans=0;
for(int i=0;i<=w;i++)
{
for(int j=0;j<=w+v;j++)
{
ans=max(ans,dp[i][j]);
}
}
cout<<ans<<"\n";
return 0;
}
/*
5 10 10
0 1 8
2 3 9
4 5 7
10 10 10
5 5 8
25
*/
总结
这套卷子我第一遍做一个小时出头是 154 p t s 154pts 154pts,比预期低一些。主要是因为我在思考简单分任务时“太想当然”。比如不能像数学一样用斜率截距区分不同直线、反向由 E E E归 1 1 1而不是 0 0 0、直接减 k k k可能更优、如果 p p p是1要特判、手写GCD要考虑负数、背包不能反向转化以及只存总消耗…
不过通过这套卷子我还是提升了很多:手写GCD(好吧之前不会)、转化思维进一步提升、推导能力提升、做题直觉更清晰…
希望我能继续保持!加油。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)