2025.8.3.模拟赛总结

原题戳我

T1

灵感这种东西,真的是听玄幻的(雾)

题目不难,就简单写写我做题时候的脑路历程吧

最开始是读题读错了,以为是每次是插入到当前排名为第ei的小朋友后面,然后就自然而然想到直接是拿最大值减去最小值就有答案了......当然没有这么美好

结果打开一大样例一对了一下,诶,不对!

重新读了一遍题目之后,又想到说,我每次插入的时候都当前把最小值扩展到它左右两个小朋友的值之中的较小值去,再把本次更新的值加到 ans 中,等到整个环都是一个数为止就行了

这次的思路逻辑是对的,但敲出来之后每次输入一个新小朋友的时候代码都会把这个流程又走一遍,结果就是只拿了40分,剩下点全都TLE了()

(说起来好像题目读取得有点问题有个特殊性质没读出来,不过问题不大

鉴于我 拿到分就行了别纠结满分不当 思想,拿完四十分之后就跑去做后面的题了()

结果发现自己剩下三道题根本做不出来只能骗分......

于是又回来研究第一题了(笑)

再好好看题,既然每次插入小朋友都会更新答案的值,那我们能不能根据之前已经计算好的值来动态维护答案呢?

当然可以

模拟一下,开始时只有1号节点,环中只有一个自环

对于每个新加入的节点,将其插入到指定节点的后面

删除旧边:移除插入位置原有的边(即插入节点与其后继节点之间的边)
添加新边:添加插入节点与前后节点之间的两条新边

根据删除和添加的边的差值......好像不对啊怎么还带负数玩的?

欸,然后我就突然想到昨天我做的一道题用到的一个概念:绝对值

重新来一遍

初始化环:开始时只有1号节点,环中只有一个自环,相邻节点差值的绝对值之和为0

插入新节点:对于每个新加入的节点,将其插入到指定节点的后面

更新差值总和:根据删除和添加的边的差值,更新环上相邻节点差值的绝对值总和

接着就又卡住了......

好像求出来的值跟答案......相差甚远啊......吗?

欸,大样例里头 678 903 407 824 300 (为了方便直接按照顺序把链排好了)的标准输出是 1020 ,我们用以上流程来一轮算出来的是 2040 ,两倍欸!

会不会答案就是我们刚刚所求的值/2?

验证一下......好像确实是这么回事啊!

最终在考场上,我在并未完全参透原理的情况下把代码给敲了出来

然后吃中午饭的时候研究了一下,其实就是利用了环的性质:
变量 \(S\) 维护的是当前圈中所有有向边的计数器值差的绝对值之和
具体来说:
对于圈中的每个小朋友 i,计算 \(|a[i] - a[nxt[i]]|\)(即当前小朋友计数器值与下一个小朋友计数器值的绝对差)
S 是所有这样的绝对值之和:\(S = \sum_{i=1}^{k} |a_i - a_nxt[i]|\)
其中 k 是当前圈中的小朋友数量(从 2 开始逐步增加)
而在环形结构中,每条边都被计算两次(物理上相邻的一对节点在环中形成两条有向边)
例如两个节点时:\(S = |a_1-a_2| + |a_2-a_1| = 2 \times |a_1-a_2|\)
输出 \(S/2 = |a_1-a_2|\) 即为答案
每次按按钮操作会使一个连通块的值整体 +1
最优策略:总是操作当前圈中的"低谷"(计数器值小于等于相邻节点的连通块)
每次这样的操作会使总差值 \(S\) 减少 2:减少与左侧邻居的差值 1 和减少与右侧邻居的差值 1
模拟一下,当加入新小朋友 i 时:

S-=llabs(a[y]-a[z]);  // 删除原有边 y→z 的贡献
S+=llabs(a[y]-a[i]);  // 增加新边 y→i 的贡献
S+=llabs(a[i]-a[z]);  // 增加新边 i→z 的贡献

这等价于:
移除原有边 \(y \rightarrow z\) 的差值 \(|a_y-a_z|\)
新增两条边:
\(y \rightarrow i\) 的差值 \(|a_y-a_i|\)
\(i \rightarrow z\) 的差值 \(|a_i-a_z|\)
此时当前圈的总差值和 \(S\) 恰好包含所有相邻节点对的差值(每个物理相邻对通过两条有向边表示)
所以 \(S/2\) 就是当前状态下使所有相邻节点值相同的最少操作次数

注意,还有一个坑点!

这题数据又大又多,别用 cincout

代码流程总结

用循环链表动态维护小朋友的圈,每次加入新小朋友时:

读入插入位置 pos

定位 y = pos 和 z = nxt[y]

更新 S:移除 y-z 边的贡献,添加 y-i 和 i-z 边的贡献

更新链表:在 y 和 z 之间插入 i

输出 S/2 作为当前状态的最少按钮次数

代码实现

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN=1000005;
ll a[MAXN];
int nxt[MAXN];
int main()
{
	//freopen("game.in","r",stdin);
	//freopen("game.out","w",stdout);
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%lld",&a[i]);
    nxt[1]=1;
    ll S=0;
    for(int i=2;i<=n;i++)
    {
        int pos;
        scanf("%d",&pos);
        int y=pos;
        int z=nxt[y];
        S-=llabs(a[y]-a[z]);
        S+=llabs(a[y]-a[i]);
        S+=llabs(a[i]-a[z]);
        nxt[y]=i;
        nxt[i]=z;
        printf("%lld\n",S/2);
    }
	//fclose(stdin);
	//fclose(stdout);
    return 0;
}

T2

看到题目的第一眼首先想到的是背包,但是再看几眼就能发现他这种又买又发的模型其实跟背包没什么关系

接下来的第二个想法是动态规划,但他那个容量其实不太好决策,直觉告诉我这应该不是正解

(虽然dp好像确实能做出来)

第三个想法就是贪心了,答案也确实是贪心

但......没敲出来......

主要是想的时候的决策有问题,对于他的容量 V 没处理好决策......死活没想到单调队列我这猪脑QAQ

(其实这题要是没有容量限制顶天就是个橙题)

想了半天没个结果之后就没再消耗脑细胞了,盯紧那十分的特殊情况一,分类讨论n=1和n=2的情况,骗了 10 分后又给剩下情况敲了个随机数就跳下一题了

提一嘴,想用随机数这东西来骗分真的是太困难了......就当是心理安慰吧

代码流程总结

其实对于这道题的贪心,用单调队列来维护是比较好的办法

我们按照时间从前往后一天一天来考虑

对于第 $ i $ 天的需求,应该是在之前找一天 $ j < i $,满足从第 $ j $ 天到第 $ i $ 天,冰棍还有剩余容量,并且 $ P_j + (i - j) \times m $ 最小

在扫描过程中,使用单调队列,维护 $ P_j - j \times m $ 的最小值——如果 $ P_{j+1} - (j + 1) \times m \leq P_j - j \times m $,则在 $ j + 1 $ 天之后不可能从第 $ j $ 天购买冰棍——这组数据具有单调性

  1. 队列维护

    • 移除不满足冰箱容量的决策点
    • 队首元素即为当前最优购买决策
  2. 标记更新

    • 购买完成后,队列中所有决策的剩余容量减少
    • 通过全局标记记录容量减少量

代码实现

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN=2000010;
ll D[MAXN],P[MAXN],R[MAXN],Q[MAXN];
int main()
{
    //freopen("ice.in","r",stdin);
    //freopen("ice.out","w",stdout);
    ll n,m,V;
    scanf("%lld%lld%lld",&n,&m,&V);
    for(int i=1;i<=n;i++)
        scanf("%lld",&D[i]);
    for(int i=1;i<=n;i++)
        scanf("%lld",&P[i]);
    int l=1,r=0;
    ll tag=0,ans=0;
    for(int i=1;i<=n;i++)
    {
        while(l<=r&&P[Q[r]]-m*Q[r]>=P[i]-m*i)
            r--;
        while(D[i]&&l<=r)
        {
            ll w=min(D[i],R[Q[l]]-tag);
            ans+=w*(P[Q[l]]+m*(i-Q[l]));
            tag+=w;
            D[i]-=w;
            if(R[Q[l]]-tag==0)
                l++;
        }
        if(D[i])
            ans+=D[i]*P[i];
        Q[++r]=i;
        R[i]=V+tag;
    }
    printf("%lld\n",ans);
    //fclose(stdin);
	//fclose(stdout);
    return 0;
}

T3

做的时候在想,是不是我可以走个dp来维护这么一组“最短路”

然后发现自己整出来一个四维的超绝状态转换方程

而且测试点全WA

然后就跳过这题去坐T4了......完全做不出来......回去做T1了()

做完T1、骗完T2的分之后,我决定也骗点这题的分

观察特殊性质,欸,有一组是所有\(y[i]\)都为1啊,那分不就好骗了吗?

于是开开心心第拿了 10 分,

而题目正解其实是有点思维深度的......

首先注意到一个特殊性质:\(y[i]=1\)
容易想到,该特殊性质的最优策略就是从小往大依次检查所有仓库,答案为坐标的极差
而对于其它的数据,当出现 \(y[i]=2\) 的时候,答案肯定不比 \(y[i]=1\) 的情况小,这样我们得到了一个答案的下界
此外,我们还能发现一个答案的上界,就是坐标极差的两倍
why? 因为我们从最左侧的仓库出发走到最右侧,再回到最左侧,一定能够检查完所有的仓库
那么,有什么其它方案会位于这个上界和下界之间呢?
我们整体的趋势还是从左走到右,但是在走的过程中,会有一些小的折返,形如:
image
也就是说,可以把整个路径分为一段一段的,每一段要么直接走过去,要么做一次折返
然后我们用DP来求解, 表示考虑完了前 个关卡的答案,转移就是枚举接下来一段是直接走过去,还是做一次折返
可以用前缀最大值把DP优化到线性

代码实现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1000005;
struct Node 
{
    long long x;
    int y;
    int k;
} info[MAXN];
int main()
{
    freopen("warehouse.in", "r", stdin);
    freopen("warehouse.out", "w", stdout);
    int n;
    cin>>n;
    long long min_x_all = LLONG_MAX, max_x_all = LLONG_MIN;
    for (int i=1; i<=n; i++) 
    {
        cin>>info[i].x;
        if (info[i].x < min_x_all) 
            min_x_all = info[i].x;
        if (info[i].x > max_x_all) 
            max_x_all = info[i].x;
        cin>>info[i].y;
        if (info[i].y==2)
            cin>>info[i].k;
        else 
            info[i].k = -1;
    }
    vector<long long> type1_x;
    map<long long, long long> minLeftMap;
    for (int i=1; i<=n; i++) 
    {
        if (info[i].y==1) 
            type1_x.push_back(info[i].x);
        else 
        {
          if (info[i].y==2) 
          {
            int k = info[i].k;
            long long key_x = info[k].x;
            long long cur_x = info[i].x;
            if (cur_x < key_x) 
            {
                if (minLeftMap.find(key_x)==minLeftMap.end()) 
                    minLeftMap[key_x] = cur_x;
                else
                    if (cur_x < minLeftMap[key_x]) 
                        minLeftMap[key_x] = cur_x;
            }
         }
      }
    sort(type1_x.begin(), type1_x.end());
    long long main_dist = 0;
    if (type1_x.size()>0) 
        for (int i=1; i<type1_x.size(); i++) 
            main_dist += type1_x[i] - type1_x[i-1];
    long long branch_dist = 0;
    for (auto &kv : minLeftMap) 
        branch_dist += 2 * (kv.first - kv.second);
    long long extra_dist = 0;
    if (!type1_x.empty()) 
        extra_dist = max_x_all - type1_x.back();
    long long ans = main_dist + branch_dist + extra_dist;
    cout<<ans<<endl;
    return 0;
}

T4

自己都没解出来,还是不乱写东西糊弄人了()

附上标准题解:

骑车

首先思考最大收益。当从景点 $ \alpha $ 走到景点 $ \beta $ 时,如果 $ p_\alpha > p_\beta $,则一定会带着一瓶饮料过去,获得 $ p_\alpha - p_\beta $ 的收益;如果 $ p_\alpha < p_\beta $,则不可能带饮料过去。因此,>对于路径经>过的点 $ z_1, z_2, \ldots, z_l $,最大收益为:

\(\sum_{i=1}^{l-1} \max(p_{z_{i+1}} - p_{z_i}, 0)\)

方案数分析
  • 当 $ p_\alpha \neq p_\beta $ 时,最优方案唯一。
  • 当 $ p_\alpha = p_\beta $ 时,可选择带或不带饮料(两种方案)。
    因此,方案总数可表示为:

\(\sum_{i=1}^{l-1} |p_{z_{i+1}} - p_{z_i}|\)

此解法可获得 20 分。

优化思路
  1. 预处理
    路径固定不变,仅价格会修改。预处理树上每条边的出现次数
  2. 部分分($ Q = 0 $)
    用前缀和维护每条路径的答案。
  3. 满分优化
  • 对每个点维护平衡树,按子节点价格排序。
  • 存储从该点到子节点的边的出现次数之和
  • 修改某点价格时,只需重新计算该点与父节点、子节点的关联答案。

简洁的结尾

posted @ 2025-08-03 19:28  Kaos·Abraham  阅读(13)  评论(0)    收藏  举报