括号画家

达达是一名漫画家,她有一个奇特的爱好,就是在纸上画括号。
这一天,刚刚起床的达达画了一排括号序列,其中包含小括号 ( )、中括号 [ ] 和大括号 { },总长度为 0x18 总结与练习 - 图1
这排随意绘制的括号序列显得杂乱无章,于是达达定义了什么样的括号序列是美观的:

  1. 空的括号序列是美观的;
  2. 若括号序列 0x18 总结与练习 - 图2 是美观的,则括号序列 0x18 总结与练习 - 图3#card=math&code=%28A%29&id=K0sf9)、0x18 总结与练习 - 图40x18 总结与练习 - 图5 也是美观的;
  3. 若括号序列 0x18 总结与练习 - 图6 都是美观的,则括号序列 0x18 总结与练习 - 图7 也是美观的。

例如 (){} 是美观的括号序列,而)({)[}]( 则不是。
现在达达想在她绘制的括号序列中,找出其中连续的一段,满足这段子串是美观的,并且长度尽量大。
你能帮帮她吗?

输入格式
输入一行由括号组成的字符串。
输出格式
输出一个整数,表示最长的美观的子段的长度。
数据范围
字符串长度不超过 0x18 总结与练习 - 图8
输入样例:

  1. ({({(({()}})}{())})})[){{{([)()((()]]}])[{)]}{[}{)

输出样例:

  1. 4

栈 + 动态规划
0x18 总结与练习 - 图9表示以 i 为结尾,最长的美观括号序列长度。
当我们使用栈去给每个右括号匹配左括号时,可以知道左括号的位置,然后顺便做转移即可。
栈里面需要保存括号类型以及对应的下标,也可以只保存下标,但编码会稍复杂一点。

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. const int N = 100010;
  4. char s[N];
  5. int st[N],top,d[N];
  6. int id[200];
  7. int main(){
  8. scanf("%s",s+1);
  9. int n = strlen(s+1);
  10. int res = 0;
  11. id['('] = -1;id[')'] = 1;
  12. id['['] = -2;id[']'] = 2;
  13. id['{'] = -3;id['}'] = 3;
  14. for(int i=1;i<=n;i++){
  15. if(id[s[i]] < 0){
  16. st[++top] = i;
  17. }else{
  18. if(top == 0 || id[s[st[top]]] + id[s[i]] != 0){
  19. top = 0; continue;
  20. }else{
  21. d[i] = i - st[top] + 1;
  22. top--;
  23. }
  24. }
  25. }
  26. for(int i=1;i<=n;i++){
  27. d[i] += d[i-d[i]];
  28. res = max(d[i],res);
  29. }
  30. cout << res << endl;
  31. return 0;
  32. }

表达式计算4

给出一个表达式,其中运算符仅包含+,-,,/,^(加 减 乘 整除 乘方)要求求出表达式的最终值。数据可能会出现括号情况,还有可能出现多余括号情况。数据保证不会出现大于或等于0x18 总结与练习 - 图10的答案。数据可能会出现负数情况。
输入格式
输入仅一行,即为表达式。
输出格式
输出仅一行,既为表达式算出的结果。
*输入样例:

  1. (2+2)^(1+1)

输出样例:

  1. 16

本题难度较大,主要是坑点多。

回顾:表达式计算的通常思路是将中缀表达式转换为后缀表达式,每次当运算符入栈时,如果栈顶运算符的优先级比当前运算符的优先级要大,那么我们会优先计算栈顶运算符,直到该条件不被满足。

本题中,运算符优先级由高到低依次是:^,*,/,+,-
另外特殊情况是:

  1. 多余括号,容易想到多余的左括号其实不需要被管理,那么多余的右括号呢?为了方便处理,我们再表达式开头补充若干个左括号即可。
  2. 存在负数,即 3 + -2其中的 -2 的 - 号前面是一个运算符,因此该处的 - 需要被认为是一个负数。另外,如果减号后面是一个左括号,那么这时我们只需要将该操作变为 -1 * 即可,方便统一处理。
  3. 开头如果出现负号,比如 -2^2,需要将其改为 0-2^2,因为此处的负号不能简单的与后面的内容合并为一个负数整体。
  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. string s;
  4. stack<int> nums;
  5. stack<int> ops;
  6. int power(int a, int b) {
  7. int rs = 1;
  8. for(;b;b>>=1) {
  9. if(b & 1) rs *= a;
  10. a = a * a;
  11. }
  12. return rs;
  13. }
  14. void calc() {
  15. int a = nums.top(); nums.pop();
  16. int b = nums.top(); nums.pop();
  17. char c = ops.top(); ops.pop();
  18. int d = 0;
  19. if(c == '+') d = a + b;
  20. else if(c == '-') d = b - a;
  21. else if(c == '*') d = a * b;
  22. else if(c == '/') d = b / a;
  23. else d = power(b, a);
  24. nums.push(d);
  25. }
  26. int main(){
  27. cin >> s;
  28. if(s[0] == '-') s = '0' + s;
  29. int cnt = 0;
  30. for(auto &c : s) {
  31. if(c == '(') cnt --;
  32. else if(c == ')') cnt ++;
  33. }
  34. if(cnt > 0) s = string(cnt, '(') + s;
  35. s = '(' + s + ')';
  36. for(int i = 0; i < s.size(); i++) {
  37. if(s[i] >= '0' && s[i] <= '9') {
  38. int j = i, x = 0;
  39. while(isdigit(s[j])) {
  40. x = x * 10 + s[j] - '0';
  41. j ++;
  42. }
  43. nums.push(x);
  44. i = j - 1;
  45. } else {
  46. auto c = s[i];
  47. if(c == '(') ops.push(c);
  48. else if(c == ')') {
  49. while(ops.top() != '(') calc();
  50. ops.pop();
  51. } else if(c == '^') {
  52. while(ops.top() == '^') calc();
  53. ops.push(c);
  54. } else if(c == '*' || c == '/') {
  55. while(ops.top() == '/' || ops.top() == '*' || ops.top() == '^') calc();
  56. ops.push(c);
  57. } else if(c == '+' || c == '-') {
  58. if(c == '-' && !isdigit(s[i - 1]) && s[i - 1] != ')') {
  59. // 此处的减号表示后面是一个负数
  60. if(s[i + 1] == '(') {
  61. nums.push(-1);
  62. ops.push('*');
  63. } else {
  64. int j = i + 1, x = 0;
  65. while(isdigit(s[j])) {
  66. x = x * 10 + s[j] - '0';
  67. j ++;
  68. }
  69. nums.push(-x);
  70. i = j - 1;
  71. }
  72. } else {
  73. // 由于所有运算符优先级都大于等于加号和等号,所以直接运算到左括号
  74. while(ops.top() != '(') calc();
  75. ops.push(c);
  76. }
  77. }
  78. }
  79. }
  80. cout << nums.top() << endl;
  81. return 0;
  82. }

城市游戏

有一天,小猫 rainbow 和 freda 来到了湘西张家界的天门山玉蟾宫,玉蟾宫宫主蓝兔盛情地款待了它们,并赐予它们一片土地。这片土地被分成 0x18 总结与练习 - 图11 个格子,每个格子里写着 R 或者 F,R 代表这块土地被赐予了 rainbow,F 代表这块土地被赐予了 freda。现在 freda 要在这里卖萌。。。它要找一块矩形土地,要求这片土地都标着 F 并且面积最大。但是 rainbow 和 freda 的 OI 水平都弱爆了,找不出这块土地,而蓝兔也想看 freda 卖萌(她显然是不会编程的……),所以它们决定,如果你找到的土地面积为 0x18 总结与练习 - 图12,它们将给你 0x18 总结与练习 - 图13 两银子。
输入格式
第一行包括两个整数 0x18 总结与练习 - 图14,表示矩形土地有 0x18 总结与练习 - 图150x18 总结与练习 - 图16 列。接下来 0x18 总结与练习 - 图17 行,每行 0x18 总结与练习 - 图18 个用空格隔开的字符 F 或 R,描述了矩形土地。每行末尾没有多余空格。
输出格式
输出一个整数,表示你能得到多少银子,即(0x18 总结与练习 - 图19最大 F 矩形土地面积)的值。
数据范围
0x18 总结与练习 - 图20
输入样例:

  1. 5 6
  2. R F F F F F
  3. F F F F F F
  4. R R R F F F
  5. F F F F F F
  6. F F F F F F

输出样例:

  1. 45

该题目可以转换为:
给定一个 01 矩阵,求最大的全1矩阵面积。
法一:暴力
枚举任意一个点的复杂度为 0x18 总结与练习 - 图21,所以枚举两个点,再加上检测的复杂度时间,总体复杂度为 0x18 总结与练习 - 图22
法二:前缀和优化
考虑到全 1 矩阵,包含 1 的个数确定,所以可以优化到 0x18 总结与练习 - 图23,仍然无法承受。
法三:换方向枚举
枚举全1矩阵的上下边界,然后将二维数组压缩到一维去处理,变为最大子段和问题,复杂度 0x18 总结与练习 - 图240x18 总结与练习 - 图25
法四:单调栈
枚举全 1 矩阵的下边界 i,然后 0x18 总结与练习 - 图26表示以 0x18 总结与练习 - 图27开始,从上走最多有多少个连续的 1 。然后在 0x18 总结与练习 - 图28数组中找最大矩形。复杂度 0x18 总结与练习 - 图29

其他:注意到题目输入是单个 char,所以最好用 cin 来输入,避免了手动getcchar处理空格换行等问题。
另外可以用 ios::sync_with_stdio(false); cin.tie(nullptr); 来关闭 cin 同步,加快输入。

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. const int N = 1010;
  4. int a[N][N];
  5. int n, m;
  6. int st[N], h[N], tp, w[N];
  7. int main(){
  8. ios::sync_with_stdio(false); cin.tie(nullptr);
  9. cin >> n >> m;
  10. for(int i=1;i<=n;i++){
  11. for(int j=1;j<=m;j++){
  12. char c;cin >> c;
  13. if(c == 'F')a[i][j] = 1;
  14. }
  15. }
  16. int res = 0;
  17. for(int i=1;i<=n;i++){
  18. tp = 0;
  19. for(int j=1;j<=m+1;j++){
  20. if(a[i][j]) h[j] = h[j] + 1;
  21. else h[j] = 0;
  22. int width = 0;
  23. while(tp && st[tp] >= h[j]){
  24. width += w[tp];
  25. res = max(res,width * st[tp]);
  26. tp--;
  27. }
  28. width ++;
  29. st[++tp] = h[j];
  30. w[tp] = width;
  31. }
  32. }
  33. cout << res * 3 << endl;
  34. return 0;
  35. }

双栈排序

Tom 最近在研究一个有趣的排序问题。通过 0x18 总结与练习 - 图30 个栈 0x18 总结与练习 - 图310x18 总结与练习 - 图32,Tom 希望借助以下 0x18 总结与练习 - 图33 种操作实现将输入序列升序排序。

  • 操作0x18 总结与练习 - 图34:如果输入序列不为空,将第一个元素压入栈 0x18 总结与练习 - 图35
  • 操作0x18 总结与练习 - 图36:如果栈 0x18 总结与练习 - 图37 不为空,将 0x18 总结与练习 - 图38 栈顶元素弹出至输出序列
  • 操作0x18 总结与练习 - 图39:如果输入序列不为空,将第一个元素压入栈 0x18 总结与练习 - 图40
  • 操作0x18 总结与练习 - 图41:如果栈 0x18 总结与练习 - 图42 不为空,将 0x18 总结与练习 - 图43 栈顶元素弹出至输出序列

如果一个 0x18 总结与练习 - 图44 的排列 0x18 总结与练习 - 图45 可以通过一系列操作使得输出序列为 0x18 总结与练习 - 图46%2C%20n#card=math&code=1%2C%202%2C%E2%80%A6%2C%28n-1%29%2C%20n&id=K8ovz),Tom 就称 0x18 总结与练习 - 图47 是一个”可双栈排序排列”。
例如 0x18 总结与练习 - 图48#card=math&code=%281%2C%203%2C%202%2C%204%29&id=wb25f) 就是一个”可双栈排序序列”,而 0x18 总结与练习 - 图49#card=math&code=%282%2C%203%2C%204%2C%201%29&id=JeY4X) 不是。
下图描述了一个将 0x18 总结与练习 - 图50#card=math&code=%281%2C%203%2C%202%2C%204%29&id=zIk1M) 排序的操作序列:<a, c, c, b, a, d, d, b>
0x18 总结与练习 - 图51
当然,这样的操作序列有可能有几个,对于上例 0x18 总结与练习 - 图52#card=math&code=%281%2C%203%2C%202%2C%204%29&id=pNx1v),<a, c, c, b, a, d, d, b>是另外一个可行的操作序列。Tom 希望知道其中字典序最小的操作序列是什么。
输入格式
第一行是一个整数 0x18 总结与练习 - 图53。第二行有 0x18 总结与练习 - 图54 个用空格隔开的正整数,构成一个 0x18 总结与练习 - 图55 的排列。
输出格式
输出共一行,如果输入的排列不是”可双栈排序排列”,输出数字 0x18 总结与练习 - 图56。否则输出字典序最小的操作序列,每两个操作之间用空格隔开,行尾没有空格。
数据范围
0x18 总结与练习 - 图57
输入样例:

  1. 4
  2. 1 3 2 4

输出样例:

  1. a b a a b b a b

如果只有一个栈,那么操作是固定的:
从前往后遍历每个数,将该数压入栈中,如果后面的数字都比栈顶数字要大,那么栈顶元素出栈(该操作一直进行,直到条件不被满足)
因此,我们只需要思考每个数字分配到哪个栈中。
根据上面的弹出顺序,可以知道,不论是单栈还是双栈,如果一个数字后面还有比它小的数字,那么这个数字是不能够被及时弹出的。
所以,若 0x18 总结与练习 - 图58,然后有一个 0x18 总结与练习 - 图59并且 0x18 总结与练习 - 图60,那么 0x18 总结与练习 - 图610x18 总结与练习 - 图62一定不能放在同一个栈中。
因为,在 0x18 总结与练习 - 图63出栈之前,0x18 总结与练习 - 图64是不能够出栈的,因此 0x18 总结与练习 - 图65也不能在 0x18 总结与练习 - 图66后面入同一个栈。
所以,我们需要预处理出所有有矛盾的二元组 0x18 总结与练习 - 图67,然后进行二分图染色就可以确定每个元素该入哪一个栈了。

证明: 上面的论述已经证明了必要性,即答案必须是满足该性质的,然后下面证明充分性,即满足该性质的一定符合答案。 我们考虑 0x18 总结与练习 - 图68入栈时的操作,在该栈中,所有元素都是大于 0x18 总结与练习 - 图69的,因为原本该栈中那些小于 0x18 总结与练习 - 图70的元素 0x18 总结与练习 - 图71都已经出栈了(因为 0x18 总结与练习 - 图720x18 总结与练习 - 图73后面并没有比 0x18 总结与练习 - 图74小的元素了)。所以栈中一定是单调递减的,这样就一定可以构造出答案。

然后再来说说字符方案最近该怎么处理。
该题在 2020 年以前,洛谷以及 Acwing 的数据都是不够强的,在字典序最小这块有很多代码可以被 hack。
入栈操作要比出栈操作优先,另外,对第一个栈操作要比对第二个栈操作优先。
在每个元素入栈之前,是必须要保证单调性的,但出栈元素在每个时刻是被唯一指定的,所以有可能两个栈都需要出栈。
也就是说,我们原本的想法是能出栈就出栈,但现在是,由于栈 2 出栈操作不如栈 1 入栈操作,所以如果栈 2 需要出栈了也不一定要立即出栈,可以先让栈 1 入栈。所以在栈 1 入栈之前需要特殊判断。
具体来说:
当前元素要入第一个栈:

  1. 如果栈 1 栈顶要出栈,则优先出栈(栈 1 出栈是要立刻出的)
  2. 栈 1 出栈完毕,如果栈 1 栈顶要比当前元素小(不满足单调性,那么栈 2 也不得不出栈),那么此时栈 2 与栈 1 相互出栈(直到 栈 1栈顶比当前元素大)。
  3. 元素入栈

当前元素要入第二个栈:

  1. 栈 1 栈 2 能出栈就出栈
  2. 元素入栈

    1. #include <bits/stdc++.h>
    2. using namespace std;
    3. typedef long long ll;
    4. const int inf = 0x3f3f3f3f;
    5. const int N = 1010 + 5;
    6. int n, a[N], f[N];
    7. int color[N];
    8. bool g[N][N];
    9. stack<int> stk1, stk2;
    10. int now = 1;
    11. bool dfs(int u,int c){
    12. color[u] = c;
    13. for (int i = 1; i <= n;i++)
    14. if(g[u][i]){
    15. if(color[i] == c)
    16. return false;
    17. if(color[i] == -1 && !dfs(i,!c))
    18. return false;
    19. }
    20. return true;
    21. }
    22. void pop() {
    23. while (true) {
    24. if (stk1.size() && stk1.top() == now) {
    25. stk1.pop();
    26. cout << "b ";
    27. now++;
    28. }
    29. else if (stk2.size() && stk2.top() == now) {
    30. stk2.pop();
    31. cout << "d ";
    32. now++;
    33. }
    34. else break;
    35. }
    36. }
    37. int main() {
    38. cin >> n;
    39. for (int i = 1; i <= n;i++)
    40. scanf("%d", &a[i]);
    41. f[n + 1] = n + 1;
    42. memset(g, false, sizeof g);
    43. for (int i = n; i;i--)
    44. f[i] = min(f[i + 1], a[i]);
    45. for (int i = 1; i <= n;i++)
    46. for (int j = i + 1; j <= n;j++)
    47. if(a[i] < a[j] && f[j+1] < a[i])
    48. g[i][j] = g[j][i] = true;
    49. memset(color, -1, sizeof color);
    50. int flag = true;
    51. for (int i = 1; i <= n;i++){
    52. if(color[i] == -1 && !dfs(i,0)){
    53. flag = false;
    54. break;
    55. }
    56. }
    57. if(flag == false){
    58. puts("0");
    59. return 0;
    60. }
    61. for (int i = 1; i <= n; i++) {
    62. if (color[i] == 0) {
    63. while (stk1.size() && stk1.top() == now) {
    64. stk1.pop();
    65. cout << "b ";
    66. now++;
    67. }
    68. while(stk1.size() && a[i] > stk1.top()) {
    69. if (stk1.size() && stk1.top() == now) {
    70. stk1.pop();
    71. cout << "b ";
    72. now++;
    73. }
    74. else if (stk2.size() && stk2.top() == now) {
    75. stk2.pop();
    76. cout << "d ";
    77. now++;
    78. }
    79. else break;
    80. }
    81. stk1.push(a[i]);
    82. cout << "a ";
    83. }
    84. else {
    85. pop();
    86. stk2.push(a[i]);
    87. cout << "c ";
    88. }
    89. }
    90. pop();
    91. cout << endl;
    92. return 0;
    93. }

滑动窗口

给定一个大小为 0x18 总结与练习 - 图75 的数组。
有一个大小为 0x18 总结与练习 - 图76 的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 0x18 总结与练习 - 图77 个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为 [1 3 -1 -3 5 3 6 7],0x18 总结与练习 - 图780x18 总结与练习 - 图79
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
输入格式
输入包含两行。
第一行包含两个整数 0x18 总结与练习 - 图800x18 总结与练习 - 图81,分别代表数组长度和滑动窗口的长度。
第二行有 0x18 总结与练习 - 图82 个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
输入样例:

  1. 8 3
  2. 1 3 -1 -3 5 3 6 7

输出样例:

  1. -1 -3 -3 -3 3 3
  2. 3 3 5 5 6 7

最大值与最小值的问题是等价的,我们只讨论最大值的情况。
在滑动窗口问题中求解最大值,维护一个单调队列(单调递减):

  1. 对于每个位置 i,首先将所有坐标小于等于 i - k 的数字都删掉
  2. 然后 0x18 总结与练习 - 图83入队之前,将队尾元素小于等于 0x18 总结与练习 - 图84的也删掉
  3. 0x18 总结与练习 - 图85入队
  4. 此时队头元素即当前窗口的最大值元素

    1. #include <bits/stdc++.h>
    2. using namespace std;
    3. const int N = 1000010;
    4. int n, k, a[N];
    5. int q1[N], l1, r1, q2[N], l2, r2;
    6. int rs[N], rs2[N];
    7. int main(){
    8. l1 = r2 = 0;
    9. r1 = r2 = -1;
    10. scanf("%d%d", &n, &k);
    11. for(int i=1;i<=n;i++){
    12. scanf("%d", &a[i]);
    13. while(l1 <= r1 && q1[l1] <= i - k) l1 ++;
    14. while(l2 <= r2 && q2[l2] <= i - k) l2 ++;
    15. while(l1 <= r1 && a[q1[r1]] >= a[i]) r1 --;
    16. while(l2 <= r2 && a[q2[r2]] <= a[i]) r2 --;
    17. q1[++r1] = i;
    18. q2[++r2] = i;
    19. if(i >= k) {
    20. rs[i] = a[q1[l1]];
    21. rs2[i] = a[q2[l2]];
    22. }
    23. }
    24. for(int i=k;i<=n;i++) printf("%d ", rs[i]);
    25. puts("");
    26. for(int i=k;i<=n;i++) printf("%d ", rs2[i]);
    27. return 0;
    28. }

    内存分配

    内存是计算机重要的资源之一,程序运行的过程中必须对内存进行分配。
    经典的内存分配过程是这样进行的:

  5. 内存以内存单元为基本单位,每个内存单元用一个固定的整数作为标识,称为地址。地址从 0x18 总结与练习 - 图86 开始连续排列,地址相邻的内存单元被认为是逻辑上连续的。我们把从地址 0x18 总结与练习 - 图87 开始的 0x18 总结与练习 - 图88 个连续的内存单元称为首地址为 0x18 总结与练习 - 图89 长度为 0x18 总结与练习 - 图90 的地址片。

  6. 运行过程中有若干进程需要占用内存,对于每个进程有一个申请时刻 0x18 总结与练习 - 图91,需要内存单元数 0x18 总结与练习 - 图92 及运行时间 0x18 总结与练习 - 图93。在运行时间 0x18 总结与练习 - 图94 内(即 0x18 总结与练习 - 图95 时刻开始,0x18 总结与练习 - 图96 时刻结束),这 0x18 总结与练习 - 图97 个被占用的内存单元不能再被其他进程使用。
  7. 假设在T时刻有一个进程申请 0x18 总结与练习 - 图98 个单元,且运行时间为 0x18 总结与练习 - 图99,则:
    1. 0x18 总结与练习 - 图100 时刻内存中存在长度为 0x18 总结与练习 - 图101 的空闲地址片,则系统将这 0x18 总结与练习 - 图102 个空闲单元分配给该进程。若存在多个长度为 0x18 总结与练习 - 图103 个空闲地址片,则系统将首地址最小的那个空闲地址片分配给该进程。
    2. 如果 0x18 总结与练习 - 图104 时刻不存在长度为 0x18 总结与练习 - 图105 的空闲地址片,则该进程被放入一个等待队列。对于处于等待队列队头的进程,只要在任一时刻,存在长度为 0x18 总结与练习 - 图106 的空闲地址片,系统马上将该进程取出队列,并为它分配内存单元。注意,在进行内存分配处理过程中,处于等待队列队头的进程的处理优先级最高,队列中的其它进程不能先于队头进程被处理。

现在给出一系列描述进程的数据,请编写一程序模拟系统分配内存的过程。

输入格式
第一行是一个数 0x18 总结与练习 - 图107,表示总内存单元数(即地址范围从 0x18 总结与练习 - 图1080x18 总结与练习 - 图109)。
从第二行开始每行描述一个进程的三个整数 0x18 总结与练习 - 图110
最后一行用三个 0x18 总结与练习 - 图111 表示结束。
数据已按 0x18 总结与练习 - 图112 从小到大排序。
输入文件最多 0x18 总结与练习 - 图113 行,且所有数据都小于 0x18 总结与练习 - 图114
输入文件中同一行相邻两项之间用一个或多个空格隔开。

输出格式
输出包括 0x18 总结与练习 - 图115 行。
第一行是全部进程都运行完毕的时刻。
第二行是被放入过等待队列的进程总数。

输入样例:

  1. 10
  2. 1 3 10
  3. 2 4 3
  4. 3 4 4
  5. 4 1 4
  6. 5 3 4
  7. 0 0 0

输出样例:

  1. 12
  2. 2

提示
0x18 总结与练习 - 图116


该题是 NOI 1999 的题目。
已经用的空间,我们用一段一段的区间表示,由于没有交集,所以直接按照左端点排序即可。
更取巧的做法,我们直接用 map 去存这些区间 map[l] = r 表示左端点为 l 的区间,右端点为 r。由于 map 内部是红黑树,所以是按照 l 排好序的。
每次插入一个新任务,从头遍历 map,找到足够大的空间,如果找不到就放入队列。
插入成功的任务,要按照 <结束时间,区间左端点l> 的形式放入优先队列,然后及时的去删除即可。
注意,如果优先队列队头任务的结束时间是 t,那么应当把所有 t 时刻结束的任务都出队,然后考虑候补队列中的任务能否在 t + 1 时刻插入到内存中。这一操作必须及时进行。需要双层 while 来实现。

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. #define dbg(x...) do { cout << "\033[32;1m" << #x <<" -> "; err(x); } while (0)
  4. void err() { cout << "\033[39;0m" << endl; }
  5. template<class T, class... Ts> void err(const T& arg,const Ts&... args) { cout << arg << " "; err(args...); }
  6. #define fi first
  7. #define se second
  8. #define pb push_back
  9. #define rep(i,j,k) for (int i = int(j); i <= int(k); i++)
  10. #define per(i,j,k) for(int i=int(j);i>=int(k);i--)
  11. #define all(V) V.begin(), V.end()
  12. typedef unsigned long long ull;
  13. typedef long long ll;
  14. typedef pair<int,int> PII;
  15. typedef pair<long long, long long> pll;
  16. int n, t, m, p, cnt, res;
  17. map<int, int> mp;
  18. queue<PII> q;
  19. priority_queue<PII, vector<PII>, greater<PII>> pq;
  20. bool add(int t, int m, int p) {
  21. for(auto it = mp.begin(); it != mp.end(); it++){
  22. auto jt = next(it);
  23. if(jt != mp.end() && (jt->first - it->second - 1 >= m)) {
  24. mp[it->second + 1] = it->second + m;
  25. pq.push({t + p, it->second + 1});
  26. return true;
  27. }
  28. }
  29. return false;
  30. }
  31. void finish(int t) {
  32. while(pq.size() && pq.top().first <= t) {
  33. int curt = pq.top().first;
  34. while(pq.size() && pq.top().first == curt) {
  35. auto top = pq.top();
  36. pq.pop();
  37. mp.erase(top.second);
  38. }
  39. res = curt;
  40. while(q.size()) {
  41. auto front = q.front();
  42. if(add(curt, front.first, front.second)) {
  43. q.pop();
  44. } else break;
  45. }
  46. }
  47. }
  48. void solve() {
  49. cin >> n;
  50. mp[-1] = -1; mp[n] = n;
  51. while(cin >> t >> m >> p) {
  52. if(!t && !m && !p) break;
  53. finish(t);
  54. if(!add(t, m, p)) {
  55. q.push({m, p});
  56. cnt ++;
  57. }
  58. }
  59. finish(2E9);
  60. cout << res << "\n" << cnt << "\n";
  61. }
  62. int main(){
  63. ios::sync_with_stdio(false); cin.tie(nullptr);
  64. int T; T = 1;
  65. while(T--) {
  66. solve();
  67. }
  68. }

矩阵

给定一个 0x18 总结与练习 - 图1170x18 总结与练习 - 图118 列的 0x18 总结与练习 - 图119 矩阵(只包含数字 0x18 总结与练习 - 图1200x18 总结与练习 - 图121 的矩阵),再执行 0x18 总结与练习 - 图122 次询问,每次询问给出一个 0x18 总结与练习 - 图1230x18 总结与练习 - 图124 列的 0x18 总结与练习 - 图125 矩阵,求该矩阵是否在原矩阵中出现过。

输入格式
第一行四个整数 0x18 总结与练习 - 图126
接下来一个 0x18 总结与练习 - 图1270x18 总结与练习 - 图128 列的 0x18 总结与练习 - 图129 矩阵,数字之间没有空格。
接下来一个整数 0x18 总结与练习 - 图130
接下来 0x18 总结与练习 - 图1310x18 总结与练习 - 图1320x18 总结与练习 - 图133 列的 0x18 总结与练习 - 图134 矩阵,数字之间没有空格。

输出格式
对于每个询问,输出 0x18 总结与练习 - 图135 表示出现过,0x18 总结与练习 - 图136 表示没有出现过。
数据范围
0x18 总结与练习 - 图1370x18 总结与练习 - 图1380x18 总结与练习 - 图139
输入样例:

  1. 3 3 2 2
  2. 111
  3. 000
  4. 111
  5. 3
  6. 11
  7. 00
  8. 11
  9. 11
  10. 00
  11. 11

输出样例:

  1. 1
  2. 0
  3. 1

二维字符串哈希
首先,我们只对第0x18 总结与练习 - 图140行处理,0x18 总结与练习 - 图141表示 0x18 总结与练习 - 图142这一前缀的哈希值。0x18 总结与练习 - 图143
然后,我们在对第0x18 总结与练习 - 图144列处理,0x18 总结与练习 - 图145
那么如何来求一个子矩阵的哈希?
对于以 0x18 总结与练习 - 图146为右下角,x 轴长度为 a,y 轴长度为 b 的子矩阵:
0x18 总结与练习 - 图147
至此,提前将所有固定长度的子矩阵存入哈希表(set 或map),然后每次查询时,直接去查哈希表是否存在即可。

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. typedef unsigned long long ull;
  4. const int N = 1010;
  5. const int base1 = 131,base2 = 13331;
  6. int n,m,a,b,q;
  7. char s[N][N];
  8. ull d[N][N],d2[N][N],p1[N],p2[N];
  9. set<ull> st;
  10. inline ull calc(int x,int y){//a b
  11. return d[x][y] - d[x-a][y] * p2[a] - d[x][y-b] * p1[b] + d[x-a][y-b] * p2[a] * p1[b];
  12. }
  13. int main(){
  14. scanf("%d%d%d%d",&n,&m,&a,&b);
  15. p1[0] = p2[0] = 1;
  16. for(int i=1;i<=n;i++)p1[i] = p1[i-1] * base1;
  17. for(int i=1;i<=m;i++)p2[i] = p2[i-1] * base2;
  18. for(int i=1;i<=n;i++){
  19. scanf("%s",s[i]+1);
  20. for(int j=1;j<=m;j++){
  21. d[i][j] = d[i][j-1] * base1 + s[i][j];
  22. }
  23. }
  24. for(int i=1;i<=n;i++){
  25. for(int j=1;j<=m;j++){
  26. d[i][j] = d[i-1][j] * base2 + d[i][j];
  27. if(i >= a && j >= b)st.insert(calc(i,j));
  28. }
  29. }
  30. scanf("%d",&q);
  31. while(q--){
  32. for(int i=1;i<=a;i++){
  33. scanf("%s",s[i] + 1);
  34. for(int j=1;j<=b;j++){
  35. d[i][j] = d[i][j-1] * base1 + s[i][j];
  36. }
  37. }
  38. for(int i=1;i<=a;i++){
  39. for(int j=1;j<=m;j++){
  40. d[i][j] = d[i-1][j] * base2 + d[i][j];
  41. }
  42. }
  43. if(st.find(d[a][b]) != st.end()){
  44. puts("1");
  45. }else puts("0");
  46. }
  47. return 0;
  48. }

树形地铁系统

一些主要城市拥有树形的地铁系统,即在任何一对车站之间,有且只有一种方式可以乘坐地铁。
此外,这些城市大多数都有一个中央车站。
想象一下,你是一名在拥有树形地铁系统的城市游玩的游客,你想探索该城市完整的地铁线路。
你从中央车站出发,随机选择一条地铁线,然后乘坐地铁行进。
每次到达一个车站,你都将选择一条尚未乘坐过的地铁线路进行乘坐。
如果不存在未乘坐过的线路,则退回到上一个车站,再做选择。
直到你将所有地铁线路都乘坐过两次(往返各一次),此时你将回到中央车站。
之后,你以一种特殊的方式回忆自己的坐车过程,你将你的完整地铁乘坐路线编码为一个二进制字符串。
其中 0x18 总结与练习 - 图148 编码表示你乘坐地铁线路到达距离中央车站更远的一站,0x18 总结与练习 - 图149 编码表示你乘坐地铁线路到达距离中央车站更近的一站。
0x18 总结与练习 - 图150

输入格式
第一行输入一个正整数 0x18 总结与练习 - 图151,代表测试用例数量。
每个测试用例由两行组成,每行输入一个由字符 0x18 总结与练习 - 图1520x18 总结与练习 - 图153 构成的字符串,长度最多为 0x18 总结与练习 - 图154, 两个字符串都描述了一种树形地铁系统的正确探索路线。

输出格式
对于每个测试用例,如果两个字符串描述的探索路线可以视为同一个地铁系统的两种探索路线,则输出 same。
否则,输出 different。
每行输出一个结果。

输入样例:

  1. 2
  2. 0010011101001011
  3. 0100011011001011
  4. 0100101100100111
  5. 0011000111010101

输出样例:

  1. same
  2. different

回想树的 DFS 序。
image.png
每个节点都会出现两次,对应进和出,也就是本题中的 0 和 1。
对于当前根节点的每个子树,我们不妨,将这些子树的 DFS 序,按照字典序排好序。那么如果两个树可以是同构的(已经提前指定了对应根的前提下),那么排好序的 DFS 也是相同的。

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. int T;
  4. string dfs(string& x,int &u){
  5. vector<string> v;
  6. u++; // 根进
  7. while(x[u] == '0')v.push_back(dfs(x,u));
  8. u++; // 根出
  9. sort(v.begin(),v.end());
  10. string res = "0";
  11. for(string s : v) res += s;
  12. res += '1';
  13. return res;
  14. }
  15. int main(){
  16. scanf("%d",&T);
  17. while(T--){
  18. string a,b;
  19. cin >> a >> b;
  20. // 由于根也需要一次进出,所以在头尾需要补齐。
  21. a = '0' + a + '1';
  22. b = '0' + b + '1';
  23. int ua = 0,ub = 0;
  24. if(dfs(a,ua) == dfs(b,ub))puts("same");
  25. else puts("different");
  26. }
  27. return 0;
  28. }

项链

有一天,达达捡了一条价值连城的宝石项链,但是,一个严重的问题是,他并不知道项链的主人是谁!
在得知此事后,很多人向达达发来了很多邮件,都说项链是自己的,要求他归还(显然其中最多只有一个人说了真话)。
达达要求每个人都写了一段关于自己项链的描述: 项链上的宝石用数字 0x18 总结与练习 - 图1560x18 总结与练习 - 图157 来标示。
一个对于项链的表示就是从项链的某个宝石开始,顺指针绕一圈,沿途记下经过的宝石,比如项链: 0x18 总结与练习 - 图158,它的可能的四种表示是 0x18 总结与练习 - 图159
达达现在心急如焚,于是他找到了你,希望你能够编写一个程序,判断两个给定的描述是否代表同一个项链(注意,项链是不会翻转的)。
也就是说给定两个项链的表示,判断他们是否可能是一条项链。
输入格式
输入文件只有两行,每行一个由字符 0x18 总结与练习 - 图1600x18 总结与练习 - 图161 构成的字符串,描述一个项链的表示(保证项链的长度是相等的)。
输出格式
如果两个对项链的描述不可能代表同一个项链,那么输出 No,否则的话,第一行输出一个 Yes,第二行输出该项链的字典序最小的表示。
数据范围
设项链的长度为 0x18 总结与练习 - 图1620x18 总结与练习 - 图163
输入样例:

  1. 2234342423
  2. 2423223434

输出样例:

  1. Yes
  2. 2234342423

最小表示法模版题

  1. string min_express(string& s) {
  2. int n = s.size();
  3. if(n == 0) return s;
  4. int i = 0, j = 1, k = 0;
  5. while(k < n && i < n && j < n) {
  6. if(s[(i + k) % n] == s[(j + k) % n]) k ++;
  7. else {
  8. s[(i + k) % n] > s[(j + k) % n] ? i += k + 1 : j += k + 1;
  9. if(i == j) i ++;
  10. k = 0;
  11. }
  12. }
  13. i = min(i, j);
  14. return s.substr(i) + s.substr(0, i);
  15. }
  16. void solve() {
  17. string s, t;
  18. cin >> s >> t;
  19. s = min_express(s); t = min_express(t);
  20. if(s == t) {
  21. cout << "Yes\n" << s << "\n";
  22. } else {
  23. cout << "No\n";
  24. }
  25. }

奶牛矩阵

每天早上,农夫约翰的奶牛们被挤奶的时候,都会站成一个 0x18 总结与练习 - 图1640x18 总结与练习 - 图165 列的方阵。
现在在每个奶牛的身上标注表示其品种的大写字母,则所有奶牛共同构成了一个 0x18 总结与练习 - 图1660x18 总结与练习 - 图167 列的字符矩阵。
现在给定由所有奶牛构成的矩阵,求它的最小覆盖子矩阵的面积是多少。
如果一个子矩阵无限复制扩张之后得到的矩阵能包含原来的矩阵,则称该子矩阵为覆盖子矩阵。
输入格式
0x18 总结与练习 - 图168 行:输入两个用空格隔开的整数,0x18 总结与练习 - 图1690x18 总结与练习 - 图170
0x18 总结与练习 - 图171 行:描绘由奶牛构成的 0x18 总结与练习 - 图1720x18 总结与练习 - 图173 列的矩阵,每行 0x18 总结与练习 - 图174 个字符,字符之间没有空格。
输出格式
输出最小覆盖子矩阵的面积。(每个字符的面积为 0x18 总结与练习 - 图175
数据范围
0x18 总结与练习 - 图176,
0x18 总结与练习 - 图177
输入样例:

  1. 2 5
  2. ABABA
  3. ABABA

输出样例:

  1. 2

提示
样例中给出的矩阵的最小覆盖子矩阵为 0x18 总结与练习 - 图178,面积为 0x18 总结与练习 - 图179


观察数据,大致可以使用 0x18 总结与练习 - 图180复杂度的算法。
由于最小覆盖矩阵可以无限扩展至所有,我们只需要考虑从最左上角开始的那一小块。

  1. 尝试从小到大枚举 0x18 总结与练习 - 图181,然后找到一个最小的 0x18 总结与练习 - 图182,满足对于所有行都以0x18 总结与练习 - 图183为单位构成重复单元。具体来说对于第 i 行字符串 0x18 总结与练习 - 图1840x18 总结与练习 - 图185,该操作复杂度为 0x18 总结与练习 - 图186
  2. 然后从列的角度去考虑,将每一行的字符串看做是一个元素,那么 0x18 总结与练习 - 图187行共有 0x18 总结与练习 - 图188个元素,比较每个元素是否相同需要 0x18 总结与练习 - 图189的复杂度,所以如果对这个长度为 0x18 总结与练习 - 图190的序列做 0x18 总结与练习 - 图191的话,时间复杂度为 0x18 总结与练习 - 图192
    1. const int N = 10010;
    2. const int M = 100;
    3. const int p = 131;
    4. int n, m, nxt[N];
    5. char s[N][M];
    6. bool st[M];
    7. int main()
    8. {
    9. scanf("%d%d", &n, &m);
    10. for (int i = 1; i <= n;i++){
    11. scanf("%s", s[i] + 1);
    12. for (int j = 1; j <= m;j++){
    13. bool flag = true;
    14. for (int u = j + 1; u <= m; u += j ){
    15. for (int l = 0; l < j && u+l <= m;l++){
    16. if(s[i][l+1] != s[i][u+l]){
    17. flag = false;
    18. break;
    19. }
    20. }
    21. if(!flag)
    22. break;
    23. }
    24. if(!flag)
    25. st[j] = 1;
    26. }
    27. }
    28. int width = 0;
    29. for (int i = 1; i <= m;i++)if(st[i] == 0){
    30. width = i;
    31. break;
    32. }
    33. {
    34. for (int i = 1; i <= n;i++)
    35. s[i][width + 1] = 0;
    36. for(int i=2,j=0;i<=n;i++)
    37. {
    38. while (j && strcmp(s[i]+1, s[j+1]+1))
    39. j = nxt[j];
    40. if(strcmp(s[i]+1,s[j+1]+1) == 0)j++;
    41. nxt[i] = j;
    42. }
    43. }
    44. int height = n - nxt[n];
    45. cout << height * width << endl;
    46. return 0;
    47. }

匹配统计

阿轩在纸上写了两个字符串,分别记为 0x18 总结与练习 - 图1930x18 总结与练习 - 图194
利用在数据结构与算法课上学到的知识,他很容易地求出了“字符串 0x18 总结与练习 - 图195 从任意位置开始的后缀子串”与“字符串 0x18 总结与练习 - 图196”匹配的长度。
不过阿轩是一个勤学好问的同学,他向你提出了 0x18 总结与练习 - 图197 个问题:
在每个问题中,他给定你一个整数 0x18 总结与练习 - 图198,请你告诉他有多少个位置,满足“字符串 0x18 总结与练习 - 图199 从该位置开始的后缀子串”与 0x18 总结与练习 - 图200 匹配的长度恰好为 0x18 总结与练习 - 图201
例如:0x18 总结与练习 - 图202,则 0x18 总结与练习 - 图2030x18 总结与练习 - 图2040x18 总结与练习 - 图205 个后缀子串,它们与 0x18 总结与练习 - 图206 的匹配长度分别是 0x18 总结与练习 - 图207
因此 0x18 总结与练习 - 图2080x18 总结与练习 - 图209 个位置与 0x18 总结与练习 - 图210 的匹配长度恰好为 0x18 总结与练习 - 图211,有 0x18 总结与练习 - 图212 个位置的匹配长度恰好为 0x18 总结与练习 - 图213,有 0x18 总结与练习 - 图214 个位置的匹配长度恰好为 0x18 总结与练习 - 图215

输入格式
第一行输入三个整数 0x18 总结与练习 - 图216,分别表示 0x18 总结与练习 - 图217 串长度、0x18 总结与练习 - 图218串长度、问题个数。
第二行输入字符串 0x18 总结与练习 - 图219,第三行输入字符串 0x18 总结与练习 - 图220
接下来 0x18 总结与练习 - 图221 行每行输入 0x18 总结与练习 - 图222 个整数 0x18 总结与练习 - 图223,表示一个问题。

输出格式
输出共 0x18 总结与练习 - 图224 行,依次表示每个问题的答案。

数据范围
0x18 总结与练习 - 图225

输入样例:

  1. 6 2 5
  2. aabcde
  3. ab
  4. 0
  5. 1
  6. 2
  7. 3
  8. 4

输出样例:

  1. 4
  2. 1
  3. 1
  4. 0
  5. 0

原问题要求,有多少个 A 的后缀与 B 匹配的长度恰好为 0x18 总结与练习 - 图226
由于 0x18 总结与练习 - 图227的范围并不大,我们先尝试解决,对于一个确定的 0x18 总结与练习 - 图228的后缀,它与 0x18 总结与练习 - 图229 的匹配长度,然后将其压入一个桶中记录即可。
暴力求解该问题的复杂度为 0x18 总结与练习 - 图230,我们试图寻找更快的做法。


该题需要前置知识:扩展 KMP。
扩展 KMP 需要求解一个 0x18 总结与练习 - 图231数组,0x18 总结与练习 - 图232为字符串 0x18 总结与练习 - 图233与以 0x18 总结与练习 - 图234开头的后缀的最长公共前缀。
特别的,0x18 总结与练习 - 图235。(为了后面计算,但其实按照定义应当是 0x18 总结与练习 - 图236
我们称 0x18 总结与练习 - 图237为一个匹配段,在求解过程中,我们维护一个右端点最靠右的匹配段,记作 0x18 总结与练习 - 图238

  • 0x18 总结与练习 - 图239,那么 0x18 总结与练习 - 图240,因此 0x18 总结与练习 - 图241
    • 紧接着,我们继续判断 0x18 总结与练习 - 图242是否等于 0x18 总结与练习 - 图243,然后另 z[i]++ ,只到不能扩展为止。
  • 0x18 总结与练习 - 图244,我们按照朴素算法来求 0x18 总结与练习 - 图245即可,其实就是 z[i]=0,然后不断扩展。

最后,我们尝试用 0x18 总结与练习 - 图246来更新 0x18 总结与练习 - 图247

  1. // 注:以下代码下标从 0 开始
  2. void exkmp(string& s, vector<int>& z) {
  3. int n = s.size();
  4. z.resize(n, 0);
  5. for(int i = 1, l = 0, r = 0; i < n; i++){
  6. if(i <= r) z[i] = min(r - i + 1, z[i - l]);
  7. while(i + z[i] < n && s[i + z[i]] == s[z[i]]) z[i] ++;
  8. if(i + z[i] - 1 > r) {
  9. l = i; r = i + z[i] - 1;
  10. }
  11. }
  12. }

然后我们回到本题。
我们需要先求出 0x18 总结与练习 - 图248串的 0x18 总结与练习 - 图249数组,然后尝试用它来求解问题。
然后类似的,我们设 0x18 总结与练习 - 图250为以 0x18 总结与练习 - 图251为开头的后缀与 B 的最长公共前缀,然后就可以用类似求解 z 数组的方法去求解 d 了。

  1. void exkmp(string& s, vector<int>& z) {
  2. int n = s.size();
  3. z.resize(n, 0);
  4. for(int i = 1, l = 0, r = 0; i < n; i++){
  5. if(i <= r) z[i] = min(r - i + 1, z[i - l]);
  6. while(i + z[i] < n && s[i + z[i]] == s[z[i]]) z[i] ++;
  7. if(i + z[i] - 1 > r) {
  8. l = i; r = i + z[i] - 1;
  9. }
  10. }
  11. }
  12. void solve() {
  13. int n, m, q;
  14. string s, t;
  15. cin >> n >> m >> q;
  16. cin >> s >> t;
  17. vector<int> z(m), d(n);
  18. unordered_map<int, int> mp;
  19. exkmp(t, z);
  20. for(int i = 0, l = 0, r = 0; i < n; i++) {
  21. if(i <= r) d[i] = min(r - i + 1, z[i - l]); // 注意这里的细节
  22. while(i + d[i] < n && d[i] < m && s[i + d[i]] == t[d[i]]) d[i] ++;
  23. if(i + d[i] - 1 > r) {
  24. l = i, r = i + d[i] - 1;
  25. }
  26. mp[d[i]] ++;
  27. }
  28. while(q --) {
  29. int x; cin >> x;
  30. cout << mp[x] << "\n";
  31. }
  32. }

电话列表

给出一个电话列表,如果列表中存在其中一个号码是另一个号码的前缀这一情况,那么就称这个电话列表是不兼容的。
假设电话列表如下:

  • Emergency 911
  • Alice 97 625 999
  • Bob 91 12 54 26

在此例中,报警电话号码(911)为 Bob 电话号码(91 12 54 26)的前缀,所以该列表不兼容。
输入格式
第一行输入整数 0x18 总结与练习 - 图252,表示测试用例数量。
对于每个测试用例,第一行输入整数 0x18 总结与练习 - 图253,表示电话号码数量。
接下来 0x18 总结与练习 - 图254 行,每行输入一个电话号码,号码内数字之间无空格,电话号码不超过 0x18 总结与练习 - 图255 位。
输出格式
对于每个测试用例,如果电话列表兼容,则输出 YES。
否则,输出 NO。
数据范围
0x18 总结与练习 - 图256,
0x18 总结与练习 - 图257
输入样例:

  1. 2
  2. 3
  3. 911
  4. 97625999
  5. 91125426
  6. 5
  7. 113
  8. 12340
  9. 123440
  10. 12345
  11. 98346

输出样例:

  1. NO
  2. YES

Trie 树模版题。全部插入后,一一检测即可。

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. int n,T,tr[100010][10],tot,cnt[100010];
  4. char s[100010][12];
  5. void insert(char *s){
  6. int len = strlen(s);
  7. int p = 0;
  8. for(int i=0;i<len;i++){
  9. int t = s[i] - '0';
  10. if(!tr[p][t]){
  11. tr[p][t] = ++tot;
  12. }
  13. p = tr[p][t];
  14. }
  15. cnt[p] = 1;
  16. }
  17. bool check(char *s){
  18. int len = strlen(s);
  19. int p = 0;
  20. for(int i=0;i<len-1;i++){
  21. int t = s[i] - '0';
  22. p = tr[p][t];
  23. if(cnt[p]){
  24. return true;
  25. }
  26. }
  27. p = tr[p][s[len-1]-'0'];
  28. if(cnt[p] > 1)return true;
  29. return false;
  30. }
  31. int main(){
  32. scanf("%d",&T);
  33. while(T--){
  34. for(int i=0;i<=tot;i++){
  35. memset(tr[i],0,sizeof tr[i]);
  36. cnt[i] = 0;
  37. }
  38. tot = 0;
  39. scanf("%d",&n);
  40. bool flag = false;
  41. for(int i=1;i<=n;i++){
  42. scanf("%s",s[i]);
  43. insert(s[i]);
  44. }
  45. for(int i=1;i<=n;i++){
  46. if(check(s[i])){
  47. puts("NO");
  48. flag = true;break;
  49. }
  50. }
  51. if(!flag)puts("YES");
  52. }
  53. return 0;
  54. }

黑盒子

黑盒子代表一个原始的数据库。
它可以用来储存整数数组,并且它拥有一个特殊变量 0x18 总结与练习 - 图258
在最开始,黑盒子是空的,并且 0x18 总结与练习 - 图259
现在对黑盒子进行一系列的操作处理,操作包括以下两种:

  • ADD(x):表示将 0x18 总结与练习 - 图260 加入到黑盒子中。
  • GET:使 0x18 总结与练习 - 图261 增加 0x18 总结与练习 - 图262,输出黑盒子中第 0x18 总结与练习 - 图263 小的数值(即将所有数按升序排序后的第 0x18 总结与练习 - 图264 个数)。

下面给出一个具体例子:

  1. 序号 操作 i 盒子内数(升序排列后) 输出的值
  2. 1 ADD(3) 0 3
  3. 2 GET 1 3 3
  4. 3 ADD(1) 1 1, 3
  5. 4 GET 2 1, 3 3
  6. 5 ADD(-4) 2 -4, 1, 3
  7. 6 ADD(2) 2 -4, 1, 2, 3
  8. 7 ADD(8) 2 -4, 1, 2, 3, 8
  9. 8 ADD(-1000) 2 -1000, -4, 1, 2, 3, 8
  10. 9 GET 3 -1000, -4, 1, 2, 3, 8 1
  11. 10 GET 4 -1000, -4, 1, 2, 3, 8 2
  12. 11 ADD(2) 4 -1000, -4, 1, 2, 2, 3, 8

为了方便描述,下面我们定义两个序列:

  • 0x18 总结与练习 - 图265%2CA(2)%2C%E2%80%A6%2CA(M)#card=math&code=A%281%29%2CA%282%29%2C%E2%80%A6%2CA%28M%29&id=AGb7r):这个序列由加入到黑盒子内的所有元素按加入顺序排列后得到,上例中的 0x18 总结与练习 - 图266 序列为 0x18 总结与练习 - 图267#card=math&code=%283%2C1%2C-4%2C2%2C8%2C-1000%2C2%29&id=Zn2Qu)。
  • 0x18 总结与练习 - 图268%2Cu(2)%2C%E2%80%A6%2Cu(N)#card=math&code=u%281%29%2Cu%282%29%2C%E2%80%A6%2Cu%28N%29&id=Q50X4):这个序列的第 0x18 总结与练习 - 图269 项表示的是第 0x18 总结与练习 - 图270 次 GET 操作时,盒子内元素的数量。上例中的 0x18 总结与练习 - 图271 序列为 0x18 总结与练习 - 图272#card=math&code=%281%2C2%2C6%2C6%29&id=k5Sdn)。

现在请你根据给出的序列 0x18 总结与练习 - 图2730x18 总结与练习 - 图274 求出操作过程中输出的所有数值。

输入格式
输入包括三行。
第一行包含两个整数 0x18 总结与练习 - 图2750x18 总结与练习 - 图276,表示 0x18 总结与练习 - 图277 序列和 0x18 总结与练习 - 图278 序列的长度。
第二行包含 0x18 总结与练习 - 图279 个整数,表示 0x18 总结与练习 - 图280 序列的每一个元素。
第三行包含 0x18 总结与练习 - 图281 个整数,表示 0x18 总结与练习 - 图282 序列的每一个元素。
同行每个数之间用空格隔开。

输出格式
输出操作过程中所有 GET 操作输出的数值。
每个数值占一行。
数据范围
0x18 总结与练习 - 图283%7C%20%5Cle%202%20%5Ctimes%2010%5E9#card=math&code=%7CA%28i%29%7C%20%5Cle%202%20%5Ctimes%2010%5E9&id=R8AVu),
0x18 总结与练习 - 图284,
对于所有 0x18 总结与练习 - 图2850x18 总结与练习 - 图286), 0x18 总结与练习 - 图287%20%5Cle%20M#card=math&code=p%20%5Cle%20u%28p%29%20%5Cle%20M&id=pnEhp) 成立。
输入样例:

  1. 7 4
  2. 3 1 -4 2 8 -1000 2
  3. 1 2 6 6

输出样例:

  1. 3
  2. 3
  3. 1
  4. 2

维护一个大顶堆,一个小顶堆。
大顶堆存放前 i 小的内容,小顶堆存放剩下的元素。
第一次 get 时,输出小顶堆堆顶,然后需要将该元素移动到大顶堆中。
在后面的每个时刻,需要保持小顶堆的元素个数不变。
具体来说,我们需要使用双指针表示处理两个序列的位置,然后双层循环,每次放入一个新元素后,都循环检测 u 序列去进行 get 操作。

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. const int N = 300010;
  4. int a[N],u[N];
  5. int n,m;
  6. priority_queue<int> l;
  7. priority_queue<int,vector<int>,greater<int> > r;
  8. int main(){
  9. scanf("%d%d",&n,&m);
  10. for(int i=1;i<=n;i++){
  11. scanf("%d",&a[i]);
  12. }
  13. for(int i=1;i<=m;i++)
  14. scanf("%d",&u[i]);
  15. sort(u+1,u+1+m);
  16. int i = 1,j = 1;
  17. while(i <= n || j <= m){
  18. int x = a[i];
  19. if(l.empty() || x >= r.top())r.push(x);
  20. else if(x < r.top()){
  21. l.push(x);
  22. r.push(l.top());
  23. l.pop();
  24. }
  25. while(j <= m && u[j] == i){
  26. printf("%d\n",r.top());
  27. l.push(r.top());
  28. r.pop();
  29. j++;
  30. }
  31. i++;
  32. }
  33. return 0;
  34. }

生日礼物

翰翰 0x18 总结与练习 - 图288 岁生日的时候,达达给她看了一个神奇的序列 0x18 总结与练习 - 图289
她被允许从中选择不超过 0x18 总结与练习 - 图290 个连续的部分作为自己的生日礼物。
翰翰想要知道选择元素之和的最大值。
你能帮助她吗?
输入格式
第一行包含两个整数 0x18 总结与练习 - 图291
第二行包含 0x18 总结与练习 - 图292 个整数 0x18 总结与练习 - 图293
输出格式
输出一个整数,表示答案。
数据范围
0x18 总结与练习 - 图294,
0x18 总结与练习 - 图295
输入样例:

  1. 5 2
  2. 2 -3 2 -1 2

输出样例:

  1. 5

将连续的正数和负数合并,然后共有 n 个数字。先将所有正数都选上,和为 sum,共 cnt 个。

  1. 0x18 总结与练习 - 图296则直接输出。
  2. 否则需要调整,调整的方式:
    1. 放弃选择某个正数。
    2. 选择一个负数,然后合并两端。

但并不是贪心的去每一步选择最优就好,因为可能出现后悔操作。类似于「数据备份」问题中我们使用二叉堆和链表来支持后悔操作。
将 n 个数字的绝对值全部放入堆中,从小到大考虑每个元素,不断循环,直到 0x18 总结与练习 - 图297
堆顶如果是正数 x,表示放弃 x,那么左右两端的负数会合并到一起,并将左右两边的和减去 x,插入到堆中(要将左右删除)。sum -= x, cnt --
堆顶如果是负数 x,并且它左右两边还有数字,那么该负数需要被选,并把两端的正数进行合并。sum += x, cnt --

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. typedef long long ll;
  4. typedef pair<int, int> pii;
  5. const int inf = 0x3f3f3f3f;
  6. const int N = 100000 + 5;
  7. int n, m, a[N], l[N], r[N], del[N];
  8. void remove(int p){
  9. l[r[p]] = l[p];
  10. r[l[p]] = r[p];
  11. del[p] = 1;
  12. }
  13. int main() {
  14. cin >> n >> m;
  15. int p = 1;
  16. for (int i = 1; i <= n;i++){
  17. int x;
  18. cin >> x;
  19. if(1ll * a[p] * x < 0) a[++p] = x;
  20. else a[p] += x;
  21. }
  22. n = p;
  23. int sum = 0, cnt = 0;
  24. for (int i = 1; i <= n;i++){
  25. if(a[i] > 0){
  26. sum += a[i];
  27. cnt++;
  28. }
  29. }
  30. priority_queue<pii, vector<pii>, greater<pii> > q;
  31. for (int i = 1; i <= n;i++){
  32. q.push({ abs(a[i]), i });
  33. l[i] = i - 1;
  34. r[i] = i + 1;
  35. }
  36. while (cnt > m) {
  37. while (del[q.top().second])
  38. q.pop();
  39. auto t = q.top();
  40. q.pop();
  41. int val = t.first, pos = t.second;
  42. if (l[pos] != 0 && r[pos] != n + 1 || a[pos] > 0) {
  43. cnt--;
  44. sum -= val;
  45. int left = l[pos], right = r[pos];
  46. a[pos] += a[left] + a[right];
  47. q.push({ abs(a[pos]), pos });
  48. remove(left);
  49. remove(right);
  50. }
  51. }
  52. cout << sum << endl;
  53. return 0;
  54. }