2026 ICPC沈阳邀请赛Vp补题

2026 ICPC沈阳邀请赛Vp补题

赛时在签到题上浪费了太多时间,没机会看更难的题,一些中档难度的题涉及的知识其实也有学过,也是非常可惜,所以这篇博客准备补一下题,并且反思一下

补题点这The 2026 ICPC China Shenyang National Invitational Programming Contest - 比赛主页 - 比赛 - QOJ.ac

难度:

榜单还是挺正的,题目难度区分度也很高

先从简单的题开始:

L. 渐隐的记忆

这道题刚开始和队友wm读了一遍题,算是有点模糊的意思,然后wm看了样例发现了规律,五分钟就直接ac了这道题,在他写代码的时候我认真理解了下题目,也是认可了wm的思路

题目:

小明有一段长度为N的字符串密码S,他记错了其中一段连续子串**L~i~,R~i~,并输入修改后的字符串A,而在区间 L~i~,R~i~外的所有位置,他输入的字符和原字符串S都一样,已知对于密码的任意字符位置 k 这个位置落在记错区间 L~i~,R~i~**内的总次数严格少于总尝试次数M 的一半,现在给小明M次输入,请帮小明还原出最开始的密码

思路:

题目的意思其实就是M个字符串中每个位置错误的次数必须小于M/2,既然错误次数小于M/2那么正确次数就要大于M/2,这时候就总结出规律对于M次尝试每个位置出现次数最多的字符就一定是正确的并且一定大于M/2,那就找出每个位置出现次数最多的,最后自然就是对的,这在wm写代码的时候我就想出来了,所以他在样例中总结出的规律也是完全正确,极速AC这道签到题

代码:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;

int n,m;
string s[N];

void solve(){
    cin>>n>>m;
    for(int i=1;i<=m;i++) cin>>s[i];
    for(int i=0;i<n;i++){
        map<char,int> mp;
        for(int j=1;j<=m;j++){
            mp[s[j][i]]++;
        }
        vector<pair<int,char>> v;
        for(auto &p : mp){
            v.push_back({p.second, p.first});
        }
        sort(v.begin(), v.end());
        cout << v.back().second;
    }
    cout << '\n';
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    int _=1;
    // cin>>_;
    while(_--){
        solve();
    }
}

AC了这道题之后榜单过的第二多的题就是K题了其次是F题,可是当时我和wm正在看F题感觉也不算难,然后另一个队友hw就在看K题,然后我们三个人就一起去看了K题,想了很多思路也是都不行,我和wm就选择先去写F题了(榜单瞬间过了很多)然后就死磕了三个小时才做出来

F. 等距矩阵

在这道题上面浪费的时间是最多的,主要原因是理解错了题目,导致思路一直是错的也wa了好几次,中途也是一直说重新读下题可是脑子里一直想的是那个错误的思路,其实在最开始的理解是对的,可是写题之后思路就错了之后最后wm去了厕所,我重新看了下题目突然就恍然大悟了,然后思路正确之后很快就ac了

题目:

思路:

题目给了八连通和四连通两种路径,一个只能上下左右,另一个可以八个方向就是四连通基础上还能斜着走,要求构造一个矩阵,使得两种路径的长度最小值一样,题目也说了八连通更灵活

所以我们最开始就想错了方向,并且写上题之后就忘记了八连通也可以走上下左右,还以为只能斜着走,我们就默认八连通最小路径自然就是走对角线,然后四连通走直角,让四连通走最小的,然后八连通的格子就是四连通(左上角到右上角再到右下角这个L型)对应位置的求和最后保证L4和L8相等(太错误了,一个斜着走一个直着走,八连通一步等于四连通两步,数还不能重复那怎么可能会是最优的)

既然八连通比四连通要灵活,那自然是不能走八连通,所以我们强制都走四连通,让四连通的长度最短,然后八连通的最短也是走四连通的走法那就符合题意了,所以就是走一个L型强制去走四连通路径,并且右上角一定要放0,因为那个位置八连通斜着走可以跳过,要使L4和L8相等,就要忽略掉右上角的数

代码:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
#define endl '\n'
#define int long long
#define pii pair<int,int>
#define fi first
#define se second
#define YES cout<<"YES"<<endl;
#define NO cout<<"NO"<<endl;

const int INF=1e6+5;
void solve()
{
	int n;
	cin>>n;
	if(n==1){
		cout<<0<<endl;
		return;
	}
	if(n==2){
		cout<<"1 0"<<endl;
		cout<<"3 2"<<endl;
		return;
	}
	vector<vector<int>> a(n+1,vector<int>(n+1,-1));
	a[1][n]=0;
	int num=1;
	vector<int> b(n*n,0);
	for(int j=1;j<=n;j++)
	{
		if(j==n) continue;
		a[1][j]=num;
		b[num]=1;
		num++;
	}
	for(int i=2;i<=n;i++)
	{
		a[i][n]=num;
		b[num]=1;
		num++;
	}
	vector<int> ans;
	for(int i=1;i<n*n;i++)
	{
		if(b[i]==0) ans.push_back(i);
	}
	int index=0;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			if(a[i][j]==-1) {
				a[i][j]=ans[index];
				index++;
			}
		}
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			if(j==1) cout<<a[i][j];
			else cout<<' '<<a[i][j];
		}
		cout<<endl;
	}
	// cout<<fixed<<setprecision(x)<<
}
signed main()
{
	IOS;
	int _=1;
	cin>>_;
	while(_--)
	solve();
	return 0;
}

K. 环线补给

在写完F题之后还剩下最后一小时才继续研究K题(现在发现这道题是这周的训练题中的一道题(VP是在上周周末))

P2629 好消息,坏消息 - 洛谷

思路:

首先第一个操作就是断环为链,这样可以方便处理对于每一个k的情况。说通俗点就是在n后面再接上1到(n-1)的值,所以数组要开双倍长度

如果从每个起始点开始遍历时间复杂度O(n2)会超时,所以预处理一个前缀和更好的去知道1到2n-1中有没有小于0的情况,然后用单调递增队列去维护区间的最小前缀和,最小前缀和减去开头如果小于零那就不合法

代码:

cpp 复制代码
#include<cstdio>
#include<iostream>
using namespace std;
int n,head=1,tail,ans;
long long a[2000001],s[2000001],q[2000001];
int main()
{
    cin>>n;
    for(int i=1;i<=n;i+=1)
        cin>>a[i];
    for(int i=1;i<=n-1;i+=1)
        a[i+n]=a[i];
    for(int i=1;i<=2*n-1;i+=1)
        s[i]=s[i-1]+a[i];
    for(int i=1;i<=2*n-1;i+=1)
    {
        while(head<=tail&&max(i-n+1,1)>q[head]) head++;
        while(head<=tail&&s[i]<=s[q[tail]]) tail--;
        q[++tail]=i;
        if(i-n+1>0&&s[q[head]]-s[i-n]>=0) ans++;
    }
    cout<<ans;
    return 0;
}

E. 时空回溯

这题赛时看了几眼,当时认为是贪心,排序后先把用时短的都做了,然后在合适的时候用卡片,同时记录一下如果出现更优解就反悔,事实这题局部最优不能逼近全局最优,使用卡片会影响你做题的顺序,可以举反例看出来

题目:

解题思路:

先用反例去看出贪心为什么不对

N = 6,时间 [2, 2, 2, 3, 3, 3]

贪心法

排序现在已经是升序的了

[2, 2, 2, 3, 3, 3]

枚举计算每一个位置使用卡牌的罚时

  • k=0: 48
  • k=1: 48 - 5*2/2 = 43
  • k=2: 48 - 4*4/2 = 40
  • k=3: 48 - 3*6/2 = 39
  • k=4: 48 - 2*9/2 = 39
  • k=5: 48 - 1*12/2 = 42
  • k=6: 48
    最小为 39.0

优解

顺序 [2, 2, 3, 2, 3, 3]

  • P[1]=2
  • P[2]=4
  • P[3]=7
  • P[4]=9
  • P[5]=12
  • P[6]=15

总罚时(不用卡片)S = 2 + 4 + 7 + 9 + 12 + 15 = 49

使用卡片在 k=3 后:

F = S - (6-3) * P[3] / 2 = 49 - 3 * 7 / 2 = 49 - 10.5 = 38.5

从这就可以看出来了,在第三个位置使用卡牌后,可以把罚时更大的题提到前面更优

这里我们可以算出每道题的权重,就是罚时的次数,对于做完k 题后使用卡牌,做题的时间序列有a1 到 ai , 就会有

  • i>k 那么ai 会被计算N-i+1次(使用卡牌后的牌都不会被减半直到最后每次罚时都会被累加)
  • i<=k 那么ai在使用卡牌前被计算k-i+1 次,使用卡牌后被计算N-k次,可是代价被减半所以相当于被计算**(N-k)/2**次

所以这些就是每道题的罚时ai 对应的权重(出现次数)

然后需要**掌握排序不等式**,两个有序的数组想要乘积和最小那应该怎么操作呢,一个升序一个降序,分别用最大的乘另一个数组中最小的,两两相乘最后的总和就是最小的

证明:

你有两组数,时间(a和b),权重(x和y)

现在a<b,x<y

那你也只有两种组合方式

1、大乘大,小乘小:b*y+a*x

2、大乘小,小乘大:b*x+a*y
(a*x + b*y) - (a*y + b*x)

= a*x - a*y + b*y - b*x

= a(x - y) + b(y - x)

= (b - a)(y - x)

这两种情况明显是第二种要更小,同序减反序明显是正数

知道了这个工具,我们就可以枚举计算对每个位置都使用卡牌后的总罚时(每道题的罚时乘上它的权重也就是出现次数~~(权重具体计算方法上面解释了)~~),最终获得最小的罚时

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

#define int long long

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    vector<int> T(n);
    for (int i = 0; i < n; i++) cin >> T[i];
    sort(T.begin(), T.end());

    int ans = LLONG_MAX;
    for (int k = 0; k <= n; k++) {
        vector<int> W(n);
        for (int i = 0; i < n; i++) {
            if (i < k)
                W[i] = 2 * (k - i) + (n - k);
            else
                W[i] = 2 * (n - i);
        }
        sort(W.begin(), W.end(), greater<int>());
        int sum = 0;
        for (int i = 0; i < n; i++) sum += T[i] * W[i];
        ans = min(ans, sum);
    }
    cout << ans / 2;
    if (ans & 1) cout << ".5\n";
    else cout << ".0\n";

    return 0;
}

后面两道难度比较大,赛时根本没做到,只好赛后补题

I. Alice的压岁钱

这题看到题目下意识就以为是博弈论,看完题目之后想用贪心,后来发现了反例就不知道咋写了,看了题解之后才知道是用dp

贪心反例:n = 9(即 M = n + 1 = 10)

  • 贪心路径(每次取最大 k = gcd(A, M)):
    1. A=1, gcd=1 → 取 1A=2
    2. A=2, gcd=2 → 取 2A=4
    3. A=4, gcd=2 → 取 2A=6
    4. A=6, gcd=2 → 取 2A=8
    5. A=8, gcd=2 → 取 2A=10
      5 步
  • 最优路径:
    1. A=1 → 2(取1)
    2. A=2 → 4(取2)
    3. A=4 → 5(取1,这里没有取2,而是少取了1
    4. A=5 → 10(因为 gcd(5,10)=5,一次取5)
      4 步

正确思路:

题目中说**A + B = n + 1 始终不变**,意味着 Bob 的硬币数 B = (n+1) - A 完全由 A 决定

于是 gcd(A, B) = gcd(A, n+1 - A) = gcd(A, n+1)

每步可拿的最大只取决于An+1的最大公约数

再通过上面的贪心反例就可以感觉出来,只考虑约数的情况即可就是最优的

M=10 约数是 [1, 2, 5, 10],那你可以1 → 2 → 4 → 5 → 10

24然后再到5需要两步,那么从 2 直接跳到 5 需要 ceil((5-2)/2) = 2 步,是一样的所以4这个状态没用

所以最优就是每一步都跳最大,到最后再调整到约数

转化成最短路径动态规划问题,贪心(局部最优)无法推出全局最优,并且当前的选择会影响后续未来的变化

dp 就是一张"最少步数记录表",每次你从当前约数出发,试着更新所有能到达的更大约数,如果发现新路径更短,就擦掉旧的、写上新的

代码:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
typedef long long ll;
ll n,m,k;
ll x[N],H[N];
ll ans[N];
vector<int> ys;

ll dp[N];

void __(){
    ys.clear();
    cin>>n;
    for(int i=1;i<=(n+1)/i;i++){//只处理到sqrt(n+1)就可以
        if((n+1)%i==0){
            ys.push_back(i);
            if(i!=(n+1)/i) ys.push_back((n+1)/i);//不是完全平方数,也放进去
        }
    }
    sort(ys.begin(),ys.end());
    // for(auto j:ys) cout<<j<<" ";
    // cout<<endl;
    
    for(int i=1;i<ys.size();i++) dp[i]=1e18;
    for(int i=0;i<ys.size()-1;i++){//以下一个约数为基准
        for(int j=i+1;j<ys.size();j++){//在当前i的情况下变成每个数的最小步数
            dp[j]=min(dp[j],dp[i]+(ys[j]-1)/ys[i]);//更新最小步数
            
            if(ys[j]%ys[i]==0) break;
        }
    }
    cout<<dp[ys.size()-1]<<"\n";
}


int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    int _=1;
    cin>>_;
    while(_--){
        __();
    }
}

感觉不是很难写,主要是判断出这道题考的什么,我正常的思考过程的话就是想模拟然后会超时,然后贪心看一下行不行,dp还没有掌握确实想不出来,这道题考的就是约数状态压缩 + DAG 最短路径 DP,感觉约数这里有点数论的知识,可是推例子的话也可以看出来

J. 霓虹天际线

题意还算好理解,把题目提取一下,就是x正半轴有好多竖着的直线,然后在y轴上找一点,过这个点做一条直线,能和x轴上那些直线的顶点相交,求最多能有多少相交

从题目中的示意图也可以看出,如果最右侧的点比左侧的点都要矮的话,那就出现看不到最右侧点的情况,因为会被左侧的点给拦住,这样可以在斜率的方向去想,x轴正半轴,i>j,Y是y轴上点的坐标

经过推导就可以求出想连到那个点需要的最小Y的高度(就是右侧点和左侧最高和距离最近的点 (与当前点相连直线最小)该支线与Y轴的交点)

我们要找的正是斜率最小的那个左侧点 ,因为它与当前点的连线在 Y 轴上的截距最大,而这个最大截距就是当前楼的可见阈值 Li

可是我们如果枚举每一个右侧点,让它与左侧点去计算时间复杂度 O(n2) 就会超时,所有想到用一个下凸包去维护(其实就是高级单调栈)

关于凸包的知识可以看我之前的这篇博客 栈&队列(单调结构总结+题型拓展讲解)-CSDN博客

这里其实是一种常见的优化方法,因为咱们对x轴上所有点对其坐标进行排序,所以斜率变化也是有规律的,既然我们要找斜率最小,那么对于左侧所有点两两依次去算斜率,比如A,B,C,如果AB的斜率大于BC,那么点B就可以扔了 ,这三点大概就是一个倒着的V型,B肯定高于A和C,所以我们要维护的是两两之间的所有斜率单调递增,这里就可以用一个下凸包

这里下凸包中所有斜率都是单调递增 的,既然有这样的单调性我们就想到用二分查找 ,其实就是每次找中间的点去算斜率,然后再缩区间

因为下凸包筛选出来的数据都是可行的(已经把那些没用的都扔了),所以在这些具有单调性的点(也可以把这些点理解为一个有序数组)用二分查找去找峰值

现在我们已经知道了每个楼需要的最小高度了,然后我们排序还是用二分,看咱输入的每个Y最多可以满足多少个楼需要的高度

到此这道题就算是写完了

代码:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

struct Point { ll x, y; };
struct Node { ll x, y; Point v; }; // 凸包点,v 为指向下一个点的向量

vector<Node> hull; // 下凸包

// 叉积符号:正逆时针,负顺时针,零共线
int cross(const Point& a, const Point& b) {
    __int128 g = (__int128)a.x * b.y - (__int128)a.y * b.x;
    return (g > 0) - (g < 0);
}

// 插入点 (x, y) 到凸包(维护下凸包,斜率单调递增)
void insert(ll x, ll y) {
    while (hull.size() > 1) {
        int m = hull.size();
        Point v1 = {x - hull[m-1].x, y - hull[m-1].y}; // 新点与倒数第二点的向量
        // 如果折线向右拐或共线,弹出倒数第一个点
        if (cross(hull[m-2].v, v1) >= 0) hull.pop_back();
        else break;
    }
    if (!hull.empty()) {
        int m = hull.size();
        hull[m-1].v = {x - hull[m-1].x, y - hull[m-1].y};
    }
    hull.push_back({x, y, {0, 1}});
}

// 在凸包上二分查找向量 v 的切点位置(返回 hull 中的下标)
int find(const Point& v) {
    int l = -1, r = (int)hull.size() - 1;
    while (r - l > 1) {
        int mid = (l + r) >> 1;
        if (cross(v, hull[mid].v) <= 0) r = mid;
        else l = mid;
    }
    return r;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int N, Q;
    cin >> N >> Q;
    vector<Point> p(N);
    for (int i = 0; i < N; ++i) cin >> p[i].x >> p[i].y;
    sort(p.begin(), p.end(), [](const Point& a, const Point& b) { return a.x < b.x; });

    vector<ll> L; // 每个大楼的可见阈值
    for (int i = 0; i < N; ++i) {
        ll x = p[i].x, y = p[i].y;
        if (i == 0) {
            // 第一栋楼在任何高度 Y>=1 都可见,最小阈值为1(题目Y最小为1)
            L.push_back(1);
        } else {
            // 二分查找最小高度 Y,使得大楼 i 可见
            ll l = 0, r = 1e12; // 上界足够大
            while (r - l > 1) {
                ll mid = (l + r) >> 1;
                Point v = {x, y - mid}; // 从 (0, mid) 到 (x,y) 的向量
                int pos = find(v);
                // 检查在凸包上,点 i 是否可见
                // 条件:mid * x >= (mid - y) * hull[pos].x + x * hull[pos].y
                // 移项后等价于:mid * (x - hull[pos].x) >= x * (hull[pos].y - y) ?
                // 直接使用整数比较避免精度问题
                __int128 lhs = (__int128)mid * x;
                __int128 rhs = (__int128)(mid - y) * hull[pos].x + (__int128)x * hull[pos].y;
                if (lhs >= rhs) r = mid;
                else l = mid;
            }
            L.push_back(r);
        }
        insert(x, y); // 将当前点加入凸包
    }

    sort(L.begin(), L.end());
    while (Q--) {
        ll Y;
        cin >> Y;
        int ans = upper_bound(L.begin(), L.end(), Y) - L.begin();
        cout << ans << '\n';
    }

    return 0;
}

赛后这两道题感觉难度还是很大的,凸包之前虽然了解过,但是如果真的写这道题也很难可以用出来,思维上很难把题目的模型提取出来,可以看出来是操作斜率但是到底怎么去优化也很难想,所以还是要多做题提升熟练度

总结

被队友打哭了

相关推荐
神威难绷泪1 小时前
数据结构:二叉树
数据结构·二叉树
风起洛阳@不良使1 小时前
中级软考(软件攻城狮)第3章知识点——数据结构与数据运算(线性结构+非线性结构)
数据结构·算法·链表
AndrewHZ1 小时前
图像处理入门009 | OpenCV 图像读取与显示:imread/imshow 全解析
图像处理·python·opencv·算法·计算机视觉·图像显示
学计算机的计算基1 小时前
TCP 传输层硬核整理:三次握手、四次挥手、拥塞控制一次讲透
java·网络·笔记·网络协议·算法
索西引擎1 小时前
【数据结构】B树与B+树:结构性质、对比分析与磁盘优化机理
数据结构·b树
xx~t1 小时前
嵌入式学习22
数据结构·学习·算法·排序算法
我是苏苏2 小时前
C#基础:不写for循环的五种方式
数据结构·算法·c#
小小龙学IT2 小时前
Google Test(gtest/gmock)开源 C++ 单元测试与 Mock 框架完全指南
c++·单元测试·开源
三8442 小时前
webshell缓存绕过/哈希碰撞
算法·哈希算法