【算法导论】WYLH 0928笔试题解

58 阅读17分钟

G9gYq88acAARaL9.jpg

RPC调用协议

有一个服务器对外提供若干个可被 RPC 调用的函数。每个函数有一个编号、一个函数名和一串参数类型描述(每个参数类型为 i 表示 4 字节整数,或 s 表示字符串)。现在给定函数表和一个以十六进制字符串表示的 RPC 数据流(可能包含多条 RPC 调用连在一起),要求把数据流解析为可读的函数调用格式并打印出来。

数据流的具体编码规则如下:

  • 整个 RPC 数据流用大写十六进制字符表示(字符集合 0-9 和 A-F),每个字节用两个十六进制字符表示。

  • 每条 RPC 调用的二进制布局为:

    1. 1 字节:函数编号(function id)。

    2. 紧跟着 为该函数所有字符串参数(s)分别给出长度的字节 —— 即若函数定义中有 k 个 s,在本条调用中会紧接着有 k 个字节,分别表示每个字符串参数的长度(以字节计),顺序与参数定义中 s 的出现顺序一致。

    3. 然后按参数顺序依次放置参数的具体数据:

    • 若参数类型为 i,占 4 字节,采用 大端(高位字节在前)表示一个整数,输出时按十进制打印。
    • 若参数类型为 s,占 len 字节(前面读到的对应长度),该 len 字节就是字符串的原始字节。
  • 假设每个函数至少有一个参数。

  • 数据流中可能包含多条 RPC 调用,解析应从头到尾依次解析直到十六进制流结束。

输入说明

  1. 第一行:正整数 nr_func,表示服务器提供的函数个数。
  2. 接下来 nr_func 行,每行描述一个函数,格式为:
func_id func_name func_args
  • func_id:整数(十进制),范围 0 ~ 255(占 1 字节)。
  • func_name:字符串(函数名,无空格)。
  • func_args:只包含字符 i 和 s 的字符串,表示参数类型的顺序,例如 "isi" 表示第 1 个参数是字符串,第 2 个是整数,第 3 个是字符串。
  1. 最后一行:一长串十六进制字符(只含 0-9 和 A-F,且为偶数长度),表示一个或多个 RPC 调用的串联编码。

输出说明

  • 依次输出数据流中每条 RPC 的可读形式,格式

    func_name(arg1,arg2,...)
    
    • 对于整型参数 i:按十进制直接输出,例如 123
    • 对于字符串参数 s:输出双引号包裹的该字符串对应的十六进制子串(不进行字符解码),例如 "6162"
  • 参数之间用英文逗号 , 分隔。函数调用之间连续输出(不另加额外空格或换行)。

测试用例

输入:

2
1 concat ss
2 add ii
0102036162414243020000000100000002

输出:

concat("6162","414243")add(1,2)

参考答案

#include <iostream>
#include <iterator>
#include <string>
#include <cassert>
#include <unordered_map>
#include <vector>
#include <queue>

inline int DecodeHex(const std::string& data, int pos, int bytes) {
    int result = 0;

    int len = bytes * 2;

    for (int i = pos; i < pos + len; ++i) {
        if ('0' <= data[i] && data[i] <= '9') {
            result = result * 16 + (data[i] - '0');
        } else if ('A' <= data[i] && data[i] <= 'F') {
            result = result * 16 + (data[i] - 'A' + 10);
        } else {
            assert(0);
        }
    }

    return result;
}

inline int ReadByte(std::string& data, int& pos) {
    auto r = DecodeHex(data, pos, 1);
    pos += 1 * 2;
    return r;
}

inline int ReadInt(std::string& data, int& pos) {
    auto r = DecodeHex(data, pos, 4);
    pos += 4 * 2;
    return r;
}

inline std::string ReadString(std::string& data, int str_len, int& pos) {
    auto r = data.substr(pos, str_len * 2);
    pos += str_len * 2;
    return r;
}

int main() {
    int nr_func; // 服务端可供客户端调用的函数个数
    std::cin >> nr_func;

    // 函数编号,函数命,参数定义
    std::unordered_map<int, std::string> func_name_map;
    std::unordered_map<int, std::string> func_args_map;
    std::unordered_map<int, int> nr_func_str_args_map;
    for (int i = 0; i < nr_func; ++i) {
        int func_id;
        std::string func_name;
        std::string func_args;
        std::cin >> func_id >> func_name >> func_args;

        for (char arg_type : func_args) {
            if (arg_type == 's') {
                ++nr_func_str_args_map[func_id];
            }
        }

        func_name_map[func_id] = std::move(func_name);
        func_args_map[func_id] = std::move(func_args);

    }

    // 16进制表示的rpc数据:
    // 1. 可能含有多条rpc数据
    // 2. 整数采用大端表示(整数高位字节排在前面)
    // 3. 每个函数最少有一个参数

    std::string input_data;
    std::cin >> input_data;

    int i = 0;
    while (i < input_data.size()) {
        // 读取一字节的rpc编号
        int func_id = ReadByte(input_data, i);

        // 获取到函数名和参数定义
        const std::string& func_name = func_name_map[func_id];
        const std::string& func_args = func_args_map[func_id];
        int nr_func_str_args = nr_func_str_args_map[func_id];

        std::queue<int> str_lens;
        for (int j = 0; j < nr_func_str_args; ++j) {
            str_lens.push(ReadByte(input_data, i));
        }

        std::cout << func_name << "(";

        for (int j = 0; j < func_args.size(); ++j) {
            char arg_type = func_args[j];

            if (arg_type == 'i') {
                int v = ReadInt(input_data, i);
                std::cout << v;
            } else if (arg_type == 's') {
                // 字符串长度
                int str_len = str_lens.front();
                str_lens.pop();

                std::string str = ReadString(input_data, str_len, i);

                std::cout << "\"" << str << "\"";

            } else {
                assert(0);
            }

            if (j != func_args.size() - 1) {
                std::cout << ",";
            }
        }

        std::cout << ")";
    }
}

海域寻宝

小明在一张 height × width 的海域网格地图上寻宝。网格上的每个格子有一个海拔高度(非负整数)。海面初始高度为 0,每小时上升 1(第 t 小时海面高度为 t)。海面上升是整体一致的,不受地形阻挡。

开始时,小明会处于一个起点,并且他知道宝藏位置在哪里。小明可以在四个正交方向(上下左右)移动,每次移动到相邻格子。只有格子被海绵淹没时(即格子的海拔 当前海面高度时),该格子才可被通过 。也就是说,必须等到海水淹没起点、终点以及起点到终点路径上的每个格子后,小明才能游过去并取得宝藏。

请问:小明至少要等待几个小时,才能取到宝藏?

输入描述

  • 第一行两个正整数:height(行数)和 width(列数)。
  • 第二行:起点坐标 start_i start_j(1 ≤ start_i ≤ height,1 ≤ start_j ≤ width)。
  • 第三行:终点坐标 target_i target_j(1 ≤ target_i ≤ height,1 ≤ target_j ≤ width)。
  • 接下来 height 行,每行 width 个非负整数,表示每个格子的海拔高度 a[i][j]

输出描述

输出一个非负整数,表示能取回宝藏的最早小时数 t

测试用例

输入:

3 3
1 1
3 3
0 2 2
1 3 4
2 2 1

输出:

2

解释:

一种满足条件的路径是 (1,1)->(2,1)->(3,1)->(3,2)->(3,3),对应高度 [0,1,2,2,1],这条路径上的最大海拔高度为 2,因此最早时间为 t = 2

二分答案+BFS验证

直接遍历枚举所有可能的t,然后用DFS/BFS验证是否存在通路,只能通过大约60%的用例,之后便会超时。

可以采用二分答案来替代暴力枚举,加速查找过程,经测试可以通过100%的用例。

#include <iostream>
#include <istream>
#include <vector>
#include <queue>

#define IN_RANGE(x, l, r) ((l) <= (x) && (x) < (r))

using Map = std::vector<std::vector<int>>;

int height, width;
int start_i, start_j;
int target_i, target_j;

std::vector<std::pair<int, int>> dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};

std::vector<std::vector<bool>> vis;

bool Check(Map& map, int t) {
    std::queue<std::pair<int, int>> q;
    
    for (int i = 0; i < height; ++i) {
        for (int j = 0; j < width; ++j) {
            vis[i][j] = false;
        }
    }

    q.push({start_i, start_j});
    vis[start_i][start_j] = true;

    while (!q.empty()) {
        auto [cur_i, cur_j] = q.front();
        q.pop();

        if (cur_i == target_i && cur_j == target_j) {
            return true;
        }

        for (auto [d_i, d_j] : dirs) {
            int next_i = cur_i + d_i;
            int next_j = cur_j + d_j;
            if (IN_RANGE(next_i, 0, height) && 
                    IN_RANGE(next_j, 0, width) && 
                    (map[next_i][next_j] - t) <= 0 && 
                    !vis[next_i][next_j]) {
                q.push({next_i, next_j});
                vis[next_i][next_j] = true;
            }
        }
    }

    return false;
}

int main() {
    std::ios_base::sync_with_stdio(true);
    std::cin.tie(NULL);
    std::cout.tie(NULL);

    std::cin >> height >> width;

    std::cin >> start_i >> start_j;
    --start_i, --start_j;

    std::cin >> target_i >> target_j;
    --target_i, --target_j;

    // map[i][j] 海拔高度
    int max_time = 0;
    Map map(height, std::vector<int>(width));
    for (int i = 0; i < height; ++i) {
        for (int j = 0; j < width; ++j) {
            std::cin >> map[i][j];
            max_time = std::max(max_time, map[i][j]);
        }
    }

    vis.assign(height, std::vector<bool>(width));

    // 初始水平面0,每小时上涨1。水面上涨不受周围障碍物阻碍。
    // 只有当水面涨到同时淹没主人公所处位置、宝藏位置,以及主人公到宝藏之前的路径时,才能取回宝藏。
    int u = std::max(map[start_i][start_j], map[target_i][target_j]);
    int v = max_time + 1;

    int ans = 0;
    // 二分答案:
    // 1. 如果在当前t的约束条件下存在通路,那么我们进一步缩小二分的右区间,探索是否存在更优化的t。
    // 2. 反之,则应该扩大二分的左区间,以确保能够搜索到至少一个有效答案。
    while (u < v) {
        int t = u + ((v - u) >> 1);
        if (Check(map, t)) {
            ans = t;
            v = t;
        } else {
            u = t + 1;
        }
    }
    std::cout << ans;
}

改造版Dijsktra

此题是一个经典的"瓶颈路问题"。此类问题根据优化目标的不同,一般有两大类型:

  1. 最小化最大值 (Minimax Path Problem) : 在所有从起点到终点的路径中,找到一条路径,使得该路径上“最难走”的一段(即权重最大的边或点)的权重尽可能小。这正是本题所属的类型,你需要最小化路径上的最高海拔。
  2. 最大化最小值 (Maximin Path Problem) : 在所有从起点到终点的路径中,找到一条路径,使得该路径上“最容易走”的一段(即权重最小的边)的权重尽可能大。这个问题也常被称为“最宽路径问题”(Widest Path Problem),可以想象成在网络中传输数据,寻找一条路径使其最小带宽尽可能大。

不难理解,"瓶颈路问题"与"最短路径问题"存在类似之处,都满足"最优子结构"和"贪心选择"性质。更进一步地,对于单源瓶颈路问题,我们可以直接使用——只需要对dist数组的定义和松弛操作进行修改即可

#include <iostream>
#include <vector>
#include <queue>
#include <array>

#define IN_RANGE(v, l, r) ((l) <= (v) && (v) < (r))

constexpr int INF = std::numeric_limits<int>::max();

std::vector<std::pair<int, int>> dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};

struct Node {
  int v;
  int i;
  int j;
  bool operator>(const Node& other) const {
    return v > other.v;
  }
};

using Map = std::vector<std::vector<int>>;

int Dijsktra(const Map& map, int height, int width, int start_i, int start_j, int target_i, int target_j) {
  std::vector<std::vector<int>> dist(height, std::vector<int>(width, INF));
  std::priority_queue<Node, std::vector<Node>, std::greater<Node>> pq;  // 最小堆

  // 初始化起点
  dist[start_i][start_j] = map[start_i][start_j];
  pq.push({
    .v = dist[start_i][start_j], 
    .i = start_i, 
    .j = start_j
  });

  while (!pq.empty()) {
    Node cur = pq.top();
    pq.pop();

    if (cur.v > dist[cur.i][cur.j]) {
      continue;
    }

    // 松弛操作
    for (auto [di, dj] : dirs) {
      int next_i = cur.i + di;
      int next_j = cur.j + dj;
      if (IN_RANGE(next_i, 0, height) && IN_RANGE(next_j, 0, width)) {
        int next_v = std::max(dist[cur.i][cur.j], map[next_i][next_j]);
        if (next_v < dist[next_i][next_j]) {
          dist[next_i][next_j] = next_v;
          pq.push({
            .v = next_v,
            .i = next_i,
            .j = next_j
          });
        }
      }
    }
  }

  return dist[target_i][target_j];
}

int main() {
  std::ios_base::sync_with_stdio(true);
  std::cin.tie(NULL);
  std::cout.tie(NULL);

  int height, width;
  int start_i, start_j, target_i, target_j;

  std::cin >> height >> width;

  std::cin >> start_i >> start_j;
  --start_i, --start_j;

  std::cin >> target_i >> target_j;
  --target_i, --target_j;

  // map[i][j] 海拔高度
  int max_time = 0;
  Map map(height, std::vector<int>(width));
  for (int i = 0; i < height; ++i) {
    for (int j = 0; j < width; ++j) {
      std::cin >> map[i][j];
      max_time = std::max(max_time, map[i][j]);
    }
  }

  std::cout << Dijsktra(map, height, width, start_i, start_j, target_i, target_j);
}

BOSS的虚弱状态

你是一名手握n种技能的游侠,正在准备与你的队友小明共同迎击一个总血气值为hp的大BOSS。

已知该BOSS的气血值处于闭区间[LowerHp, UpperHp](0<LowerHp<UpperHp<Hp<=1000)时,会进入虚弱状态。此时再由你的队友小明对它发起最终的致命一击。而让BOSS进入虚弱状态的重任,则由你承担。

请问:通过发动若干次技能(可以重复使用同一种技能),你能让BOSS进入虚弱状态吗?如果可以,至少要发动多少次技能,才能让BOSS进入虚弱状态?

输入说明

  • 第一行:测试用例数量(正整数)。
  • 每个测试用例:
    • 第一行三个整数 hp LowerHp UpperHp(0 < LowerHp < UpperHp < hp ≤ 1000)。
    • 第二行一个整数 n(1 ≤ n ≤ 100),表示技能数量。
    • 第三行 n 个正整数 d1 d2 ... dn,其中 di 表示第 i 个技能每次能减少的气血值(1 ≤ di ≤ 1000)。技能可以任意次数使用(包括 0 次)。

输出说明

对每个测试用例输出一行,表示让 BOSS 进入虚弱状态所需的最少技能次数。如果无法将 BOSS 的气血降到 [LowerHp, UpperHp] 区间内,输出 0

测试用例

输入

3
10 3 5
2
3 4
7 3 4
1
5
20 13 15
3
4 3 8

输出

2
0
2

说明:

  • 样例 1:  初始 hp = 10,目标区间 [3,5],技能为 3 和 4。例如使用两次 3 伤害:10 -> 7 -> 4,4 在区间内,攻击次数为 2(这是最少的)。输出 2
  • 样例 2:  初始 hp = 7,目标区间 [3,4],技能为单个 5。任何连续使用 5 都会使血量变为 7 -> 2(或直接为 2),无法落在 [3,4],因此输出 0
  • 样例 3:  hp = 20,目标 [13,15],技能 4,3,8。例如 20 -> 16(用 4)-> 13(用 3),共 2 次,可以达标,且无法用 1 次达标,因此最少为 2

暴力枚举DP

由于题目中的数据量非常小,直接暴力DP枚举就能过了。

#include <iostream>
#include <vector>
#include <string>
#include <cassert>

#define INF (1<<30)

int main() {
  int t;
  std::cin >> t;
  while (t-- > 0) {
    int hp, lower_hp, upper_hp;
    // 0<LowerHp<UpperHp<Hp<=1000
    std::cin >> hp >> lower_hp >> upper_hp;

    int n; // 技能数
    std::cin >> n;

    // d[i] 第i个技能可以减少的气血值
    std::vector<int> d(n);
    for (int i = 0; i < n; ++i) {
      std::cin >> d[i];
    }

    // dp[i][j] 使用前i种技能,将hp降低到j,所需要消耗的最小技能数
    std::vector<std::vector<int>> dp(n + 1, std::vector<int>(hp + 1, INF));

    // 有0种技能,将hp降到hp(即什么事情都没发生),只需要发动0次技能
    dp[0][hp] = 0;

    for (int i = 1; i <= n; ++i) {
      for (int j = 0; j <= hp; ++j) {
        // 不使用当前技能
        dp[i][j] = dp[i - 1][j];
        // 使用当前技能
        // 暴力枚举发动若干次第i种技能前,boss血量的可能值
        for (int prev_hp = hp; prev_hp >= j; --prev_hp) {
          int curr_hp = prev_hp;
          int cnt = 0;
          // 假设boss的血量已下降为prev_hp,
          // 模拟在此基础上对boss发动若干次第i种技能。
          while (curr_hp >= j) {
            // 如果检测发现,经过发动cnt次第i种技能后,
            // boss的血量可以由prev_hp下降到j,
            // 则尝试更新dp[i][j]。
            if (curr_hp == j) {
              dp[i][j] = std::min(dp[i - 1][prev_hp] + cnt, dp[i][j]);
              break;
            }
            curr_hp -= d[i - 1];
            ++cnt;
          }
        }
      }
    }

    int result = INF;
    for (int j = lower_hp; j <= upper_hp; ++j) {
      result = std::min(result, dp[n][j]);
    }

    if (result < INF) {
      std::cout << result << std::endl;
    }
    else {
      std::cout << 0 << std::endl;
    }
  }
}

完全背包问题

本题是一道类完全背包问题(n种物品都可以使用无限次),且优化目标为在达到目标价值的前提下,使得拿的物品总数尽可能地少。

这听上去是不是与leetcode上的"零钱兑换"这道题很像?

是的,我们完全可以把本题转换成"零钱兑换"这道经典面试题来解答!

#include <iostream>
#include <vector>
#include <string>
#include <cassert>

#define INF (1<<30)

int main() {
  int t;
  std::cin >> t;
  while (t-- > 0) {
    int hp, lower_hp, upper_hp;
    // 0<LowerHp<UpperHp<Hp<=1000
    std::cin >> hp >> lower_hp >> upper_hp;

    int n; // 技能数
    std::cin >> n;

    // d[i] 第i个技能可以减少的气血值
    std::vector<int> d(n);
    for (int i = 0; i < n; ++i) {
      std::cin >> d[i];
    }

    // dp[i] 对BOSS造成i点伤害,至少需要发动几次技能
    std::vector<int> dp(hp + 1, INF);
    dp[0] = 0;
    for (int cur_d : d) {
      for (int t = cur_d; t <= hp; ++t) {
        dp[t] = std::min(dp[t], dp[t - cur_d] + 1);
      }
    }

    // 遍历BOSS虚弱状态区间,找出使BOSS进入虚弱状态的最少技能数
    int ans = INF;
    for (int target = hp - upper_hp; target <= hp - lower_hp; ++target) {
      ans = std::min(ans, dp[target]);
    }

    std::cout << ((ans >= INF) ? 0 : ans) << std::endl;
  }
}

BFS

此外本题还可以转换成图的最短路径问题来求解。这也是校招笔试中很常见的一个考点和解题技巧。

比如,我们可以将BOSS的不同血量状态,看作图中的一个个不同的节点。通过释放1次技能,我们就可以从图中HP值较大的节点向HP值更小的节点转移。

本题要求的优化目标是释放技能次数最少,这意味着假如我们用图中相邻两个节点之间的那条边代表"释放一次技能",那么本题就转换成了一个从源节点(BOSS的起始HP)到目标节点(BOSS处在虚弱状态的HP)的单源最短路问题。

更进一步地,由于本题图中所有的边的长度都为1(释放1次技能即可从一个节点/HP转换到另一个节点/HP),我们可以很方便地使用BFS算法进行求解。

#include <iostream>
#include <vector>
#include <string>
#include <cassert>
#include <queue>

#define INF (1<<30)

int main() {
  int t;
  std::cin >> t;
  while (t-- > 0) {
    int hp, lower_hp, upper_hp;
    // 0<LowerHp<UpperHp<Hp<=1000
    std::cin >> hp >> lower_hp >> upper_hp;

    int n; // 技能数
    std::cin >> n;

    // d[i] 第i个技能可以减少的气血值
    std::vector<int> d(n);
    for (int i = 0; i < n; ++i) {
      std::cin >> d[i];
    }

    std::queue<int> q;
    std::vector<int> dist(hp + 1, INF);

    q.push(hp);
    dist[hp] = 0;

    while (!q.empty()) {
      int cur_hp = q.front();
      q.pop();

      for (int cur_d : d) {
        int next_hp = cur_hp - cur_d;
        if (next_hp >= 0 && dist[next_hp] == INF) {
          dist[next_hp] = dist[cur_hp] + 1;
          q.push(next_hp);
        }
      }
    }

    int ans = INF;
    for (int target = lower_hp; target <= upper_hp; ++target) {
      ans = std::min(ans, dist[target]);
    }

    std::cout << ((ans >= INF) ? 0 : ans) << std::endl;
  }
}

飞船入侵

在一次联合作战中,你指挥的 n (1 ≤ n ≤ 10^6)名玩家同时对敌方飞船释放能量炮。第 i 个玩家的能量炮在整数时刻 a[i] (0 ≤ a[i] ≤ 10^6)开始生效,持续恰好 keep_time (1 ≤ keep_time ≤ 10^6)个连续的整数时刻(也就是说,它在时刻 a[i], a[i]+1, ..., a[i]+keep_time-1 都会造成伤害)。能量炮在每个被覆盖的整数时刻对飞船造成固定的 b[i](1 ≤ b[i] ≤ 10^6)点伤害。

在任意一个整数时刻,飞船受到的总伤害等于此时刻所有处于生效状态的能量炮的 b[i] 之和。请计算飞船在所有整数时刻中所承受的最大总伤害

输入说明

  • 第一行包含两个整数:n(玩家数)和 keep_time(每个能量炮的持续时长)。

  • 接下来 n 行,每行包含两个整数 a[i] 和 b[i]

    • a[i] —— 第 i 个能量炮开始生效的整数时刻(起始时刻,包含在内)。
    • b[i] —— 第 i 个能量炮在每个被覆盖时刻造成的伤害值。

输出说明

输出一个整数,表示飞船在任意整数时刻受到的最大总伤害。

测试用例

输入1:

3 4
1 3
2 5
6 2

输出1:

8

输入2:

4 5
0 10
0 20
0 30
0 40

输出2:

100

输入3:

3 1
5 7
2 3
5 10

输出3:

17

暴力解法

只能过60%的测试用例。

性能瓶颈:在每个时间点上,都需要遍历所有玩家的能量炮,计算当前时间点的总伤害值。

#include <iostream>
#include <vector>

int main() {
    /* 我方玩家数量,能量炮持续时间 */
    int n, keep_time;
    std::cin >> n >> keep_time;

    /*
     * n行,a[i]每个能量炮对飞船造成伤害的开始时间
     * b[i] 每个能量炮对飞船造成伤害的伤害值
     */
    std::vector<int64_t>    a( n );
    std::vector<int64_t>    b( n );
    int max_time = 0;
    for ( int i = 0; i < n; ++i ) {
        std::cin >> a[i] >> b[i];
        max_time = std::max( max_time, a[i] + keep_time - 1 );
    }

    int64_t ans = 0;
    for (int current_time = 0; current_time <= max_time; ++current_time ) {
        /* 遍历所有玩家,检查哪些玩家的能量炮处于有效区间 */
        int64_t sum = 0;
        for ( int player = 0; player < n; ++player ) {
            if ( a[player] <= current_time && current_time < a[player] + keep_time ) {
                sum += b[player];
            }
        }
        ans = std::max( ans, sum );
    }

    std::cout << ans;
}

优化解法

事实上,我们不需要在每个时间点都从头计算总的伤害值。t时刻的总伤害值,相较于t-1时刻的总伤害值,只有可能当t时刻有能量炮进入启用状态,或者有能量炮的有效状态结束,才可能发生变化。

据此,我们可以借鉴差分数组/滑动窗口/扫描线的思想,构建更高效的算法。

#include <iostream>
#include <vector>
#include <algorithm>

struct Event {
  int time;
  int diff;
};

int main() {
  std::ios_base::sync_with_stdio(false);
  std::cin.tie(NULL);
  std::cout.tie(NULL);

  int n, keep_time;
  std::cin >> n >> keep_time;

  std::vector<Event> events;
  for (int i = 0; i < n; ++i) {
    Event start_event;
    std::cin >> start_event.time >> start_event.diff;

    Event end_event = {
      .time = start_event.time + keep_time,
      .diff = -start_event.diff,
    };

    events.emplace_back(start_event);
    events.emplace_back(end_event);
  }

  // 对所有事件,按时间先后排序
  std::sort(events.begin(), events.end(), [](const auto& a, const auto& b) -> bool {
    return a.time < b.time;
  });

  int64_t ans = 0;
  int64_t sum = 0;
  for (int i = 0; i < events.size(); ++i) {
    sum += events[i].diff;
    if (i + 1 == events.size() || events[i].time < events[i + 1].time) {
      ans = std::max(ans, sum);
    }
  }

  std::cout << ans;
}