总结(2019CSP之后),含题解

C S P \mathcal{CSP} CSP 爆炸 到现在,已经有 3 3 3个月了。这三个月间,我——这个小蒟蒻又接触了许多听不懂的东西


  • N o . 1 \mathcal{No.}1 No.1 字符串 h a s h \mathcal{hash} hash:

    • 定义:

      • 是把任意长度的输入通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
        e m m m emmm emmm,还是不懂,跳过
    • 特点:

      • 时间复杂度较低
      • 运用了前缀和思想
      • 对相同数据运算,得到的结果是一样的
      • 没法逆运算
    • 实现:

      • 注意:字符串 h a s h hash hash通常使用一个数组来存储 h a s h hash hash值, 并需要在计算时乘以 m o d mod mod m o d mod mod通常取一个较大质数, l i k e like like 131 131 131, 13331 13331 13331), 数组通常用 l o n g   l o n g long\ long long long
      • 步骤:
        • 首先进行初始化

          const int MAXN = 1000010;
          const long long Base = 131; //初始化MOD(我这用的是Base)
          
          char Str[MAXN]; //需处理的字符串
          long long Hash[MAXN]; //定义hash数组
          
          void Init(const int &Len) {
                     
          	for (int i = 1; i <= Len; i ++ ) {
                     
          		Hash[i] = Hash[i - 1] * Base + (Str[i] - 'a' + 1);
          	}
          	
          	return ;
          }
          

          怎么样,简不简单?

          简单个锤子!!!

        • 随后,我们有时需要查询 L e f t \mathcal{Left} Left ~ R i g h t \mathcal{Right} Right h a s h hash hash 值,但 L e f t Left Left 不一定等于 1 1 1, 怎么办?

        • 这时, 我们就需要运用前缀和思想,既然要求 L e f t \mathcal{Left} Left ~ R i g h t \mathcal{Right} Right h a s h hash hash值,我们是不是可以求出 1 1 1 ~ ( L e f t − 1 ) (\mathcal{Left - }1) (Left1) h a s h hash hash值与 1 1 1 ~ R i g h t \mathcal{Right} Right h a s h hash hash值,再进行操作,就完美解决了!!!

        • 上代码

          long long Get(const int &Left, const int &Right) {
                     
          	return Hash[Right] - Hash[Left - 1] * pow(Base, Right - Left + 1);
          }
          
        • 当然,我们会发现, G e t \mathcal{Get} Get函数中调用的的 p o w pow pow过于耗时,所以我们在初始化时可以定义一个 P o w \mathcal{Pow} Pow数组来存储, 实现代码如下:

          const int MAXN = 1000010;
          const long long Base = 131; //初始化MOD(我这用的是Base)
          
          char Str[MAXN]; //需处理的字符串
          long long Hash[MAXN], Pow[MAXN]; //定义hash数组
          
          void Init(const int &Len) {
                     
          	Pow[0] = 1; //长度为零时初始化为一
          	for (int i = 1; i <= Len; i ++ ) {
                     
          		Pow[i] = Pow[i - 1] * Base;
          		Hash[i] = Hash[i - 1] * Base + (Str[i] - 'a' + 1);
          	}
          	
          	return ;
          }
          
          long long Get(const int &Left, const int &Right) {
                     
          	return Hash[Right] - Hash[Left - 1] * Pow[Base, Right - Left + 1];
          }
          
        • 完美解决!!!

    • 例题:

      • 兔子与兔子 O J \mathcal{OJ} OJ a c w i n g acwing acwing
        • 解析:这道题十分简单,直接套 h a s h hash hash模板
        • 代码
          #include <cstdio>
          #include <cstring>
          
          const int MAXN = 1000010;
          const long long Base = 131;
          
          char Str[MAXN];
          long long Hash[MAXN], Pow[MAXN];
          
          void Init(const int &Len) {
                     
          	Pow[0] = 1;
          	for (int i = 1; i <= Len; i ++ ) {
                     
          		Pow[i] = Pow[i - 1] * Base;
          		Hash[i] = Hash[i - 1] * Base + (Str[i] - 'a' + 1);
          	}
          	
          	return ;
          }
          
          long long Get(const int &Left, const int &Right) {
                     
          	return Hash[Right] - Hash[Left - 1] * Pow[Base, Right - Left + 1];
          }
          
          int main () {
                     
          	scanf ("%s", Str + 1);
          	Len = strlen(Str + 1);
          	
          	Init(Len);
          	
          	scanf ("%d", &m);
          	
          	while (m --) {
                     
          		scanf ("%d %d %d %d", &L1, &R1, &L2, &R2);
          		
          		if (Get(L1, R1) == Get(L2, R2)) {
                     
          			puts("Yes");
          		}
          		
          		else {
                     
          			puts("No");
          		}
          	}
          	
          	return 0;
          } 
          
      • 回文子串的最大长度 O J \mathcal{OJ} OJ a c w i n g acwing acwing
        • 解法:前缀和 + 后缀和 + 二分 + h a s h hash hash 时间复杂度 O ( n ∗ l o g ( n ) ) \mathcal{O}(n * log(n)) O(nlog(n))
        • 我们发现这道题目数据范围恐怖, 那么只有几个办法可以让我们求解这道题目,那就是 h a s h hash hash, 或者是 O ( n ) \mathcal{O(n)} O(n)复杂度的 M a n a c h e r \mathcal{Manacher} Manacher算法 小蒟蒻不会
        • M a n a c h e r \mathcal{Manacher} Manacher由此进入
        • 通过兔子与兔子,我们知道判断两个字符串是否相等,可以使用字符串 h a s h hash hash也就是将字符串算成 P \mathcal{P} P进制数值,然后区间和判断即可,那么这道题目我们需要一个正的字符串,还需要一个反的字符串,然后如果正字符串等于反的字符串,那么奇数回文串就 2   +   1 2\ +\ 1 2 + 1,偶数回文串就直接 2 2 2即可.之所以要这么做,因为我们是要回文对不对,我们需要将回文拆解成为一个正字符串和一个反字符串,这样才好处理这道题目.
        • 既然如此,我们可以算出一个前缀和,再算出一个后缀和,然后就可以知道,正字符串和一个反字符串.字符串的 h a s h hash hash值就是这个区间的 h a s h hash hash值和.
        • 算完之后,我们当前就只需要枚举一个 m i d mid mid中间点,因为所有回文串都是有一个中间点(奇),或者中间区间(偶),然后二分分别寻找这个字符串长度即可,记住不是回文串,回文串的长度,是字符串长度 ∗   2   +   1 *\ 2\ +\ 1  2 + 1(奇) 或者是字符串长度   ∗   2 \ *\ 2   2(偶数).
        • 切记如果说这个最大回文串为 1 1 1(也就是所有字符都不一样,比如说 a b c d e f g abcdefg abcdefg),那么输出是 1 1 1,不是 3 3 3,奇数回文串=奇数字符串 ∗   2   +   1 *\ 2\ +\ 1  2 + 1,你们要小心特判这种情况,或者处理二分边界.
        • 代码偷懒不写注释(逃)
          #include <cstdio>
          #include <cstring>
          #include <algorithm>
          using namespace std;
          
          const int MAXN = 100010, Base = 131;
          
          int Len, Total, MaxLen, n;
          char Str[MAXN], s[MAXN];
          long long Hash_Front[MAXN], Hash_Back[MAXN], Power[MAXN] = {
                     1};
          
          long long Get1(int l, int r) {
                     
          	return Hash_Front[r] - Hash_Front[l - 1] * Power[r - l + 1];
          }
          
          long long Get2(int l, int r) {
                     
          	return Hash_Back[l] - Hash_Back[r + 1] * Power[r - l + 1];
          }
          
          bool Check1(int L, int x) {
                     
          	return Get1(x - L, x - 1) == Get2(x + 1, x + L);
          }
          
          bool Check2(int L, int x) {
                     
          	return Get1(x - L, x - 1) == Get2(x, x + L - 1);
          }
          
          void Binary_Search(int x) {
                     
          	int Left_ = 0, Right_ = min(x - 1, Len - x), Left__ = 0, Right__ = min(x - 1, Len - x + 1);
          	
          	while (Left_ < Right_) {
                     
          		int Mid = (Left_ + Right_ + 1) >> 1;
          		
          		if (Check1(Mid, x) == true) {
                     
          			Left_ = Mid;
          		}
          		
          		else {
                     
          			Right_ = Mid - 1;
          		}
          	}
          	
          	MaxLen = max(MaxLen, Left_ * 2 + 1);
          	
          	while (Left__ < Right__) {
                     
          		int Mid = (Left__ + Right__ + 1) >> 1;
          		
          		if (Check2(Mid, x) == true) {
                     
          			Left__ = Mid;
          		}
          		
          		else {
                     
          			Right__ = Mid - 1;
          		}
          	}
          	
          	MaxLen = max(MaxLen, Left__ * 2);
          	
          	return ;
          }
          
          int main () {
                     
          	for (int i = 1; i < MAXN; i ++ ) {
                     
          		Power[i] = Power[i - 1] * Base;
          	}
          	
          	while (scanf ("%s", Str + 1) != EOF and Str[1] != 'E') {
                     
          		Len = strlen(Str + 1);
          		MaxLen = 1;
          		Hash_Back[Len + 1] = 0;
          		
          		for (int i = 1; i <= Len; i ++ ) {
                     
          			Hash_Front[i] = Hash_Front[i - 1] * Base + Str[i] - 'a' + 1;
          		}
          		
          		for (int i = Len; i >= 1; i -- ) {
                     
          			Hash_Back[i] = Hash_Back[i + 1] * Base + Str[i] - 'a' + 1;
          		}
          		
          		for (int i = 1; i <= Len; i ++ ) {
                     
          			Binary_Search(i);
          		}
          		
          		printf ("Case %d: %d\n", ++ Total, MaxLen);
          		
          		getchar();
          	}
          	
          	return 0;
          } 
          
    • h a s h hash hash完结!!!

  • N o . 2   T r i e \mathcal{No.}2\ \mathcal{Trie} No.2 Trie树(主要字符串)

    • 实质(我看来):高效的存储和查找字符串集合的数据结构

    • 思想:从根节点开始,判断有没有该类字符,有就向下,没有就添加叶节点,依次存储,把所有结尾点标记一下,然后用 T r i e \mathcal{Trie} Trie高速查找某一个字符出现的次数

    • 模板(字符串):

      void Insert(char *Str) {
              //简单的初始化插入
      	int Root = 0, Len = strlen(Str + 1);
      	
      	for (int i = 1; i <= Len; i ++ ) {
             
      		int ID = Str[i] - 'a';
      		
      		if (Trie[Root][ID] == 0) {
              //如果当前字符未出现过,开一个新空间存储
      			Trie[Root][ID] = ++ Total; 
      		}
      		
      		Root = Trie[Root][ID]; //进入下一个字符
      	}
      	
      	return ;
      }
      
      bool Find(char *Str) {
              //查找是否有此单词
      	int Root = 0, Len = strlen(Str + 1);
      	
      	for (int i = 1; i <= Len; i ++ ) {
             
      		int ID = Str[i] - 'a';
      		
      		if (Trie[Root][ID] == 0) {
              //如果当前字符未匹配成功,返回找不到
      			return false;
      		}
      		
      		Root = Trie[Root][ID]; //进入下一个字符
      	}
      	
      	return true;
      }
      
    • 例题:
      • 前缀统计 O J \mathcal{OJ} OJ a c w i n g acwing acwing
        • 解析:一想到字符串的前缀,我们就应该想到 T i r e \mathcal{Tire} Tire树.这道题目是字典树模板的略微改动,我们发现这道题目和一般 T i r e \mathcal{Tire} Tire树的查询不一样, T i r e \mathcal{Tire} Tire树一般查询是看这个字符串是否出现,而这道题目这是统计这个字符串出现的次数.
        • 代码:
          #include <cstdio>
          #include <cstring>
          #include <algorithm>
          using namespace std;
          
          const int MAXN = 1e6 + 5;
          
          int n, m, Trie[MAXN][26], Total, End[MAXN];
          char Str[MAXN];
          
          void Insert(char *Str) {
                      //简单的初始化插入加处理
          	int Root = 0, Len = strlen(Str + 1);
          
          	for (int i = 1; i <= Len; i ++ ) {
                     
          		int ID = Str[i] - 'a';
          		
          		if (Trie[Root][ID] == 0) {
                      //如果当前字符未出现过,开一个新空间存储
          			Trie[Root][ID] = ++ Total; 
          		}
          		
          		Root = Trie[Root][ID]; //进入下一个字符
          	}
          	
          	End[Root] ++; //统计个数
          	
          	return ;
          }
          
          bool Find(char *Str) {
                      //查找是否有此单词
          	int Root = 0, Len = strlen(Str + 1);
          	
          	for (int i = 1; i <= Len; i ++ ) {
                     
          		int ID = Str[i] - 'a';
          		
          		if (Trie[Root][ID] == 0) {
                      //如果当前字符未匹配成功,返回找不到
          			return false;
          		}
          		
          		Root = Trie[Root][ID]; //进入下一个字符
          	}
          	
          	return true;
          }
          
          int Search(char *Str) {
                     
              int Root = 0, Sum = 0, Len = strlen(Str + 1);
              
          	for (int i = 1; i <= Len; i ++ ) {
                     
          		int ID = Str[i] - 'a';
          		Root = Trie[Root][ID];
          
          		if (Root == 0) {
                      //如果到了头,就跳出循环
          			break;
          		}
          		
          		Sum += End[Root]; //统计答案
          	}
          	
          	return Sum; //返回答案
          }
          
          
          int main () {
                     
          	scanf ("%d %d", &n, &m);
          	
          	for (int i = 1; i <= n; i ++ ) {
                     
          		scanf ("%s", Str + 1);
          		Insert(Str); //初始化
          	}
          	
          	while (m --) {
                     
          		scanf ("%s", Str + 1);
          		printf ("%d\n", Search(Str));
          	}
          	
          	return 0;
          }
          
    • T r i e \mathcal{Trie} Trie树完结!!!


  • N o . 3 \mathcal{No.}3 No.3 单调栈/单调队列:

    • 一种神奇的优化,元素在队列中是单调有序的,由于小蒟蒻理解不够透彻,接不再过多阐述
      有兴趣的同学可由此进一步了解
    • 例题:
      • 直方图中最大的矩形 (单调栈) O J \mathcal{OJ} OJ a c w i n g acwing acwing
        • 解析:这道题目是一道单调栈的好题目,首先我们发现,如果说我们确定了这个矩阵的高度的话,那么其实这个矩阵已经确定了,它的 [ l , r ] [l,r] [l,r]其实我们已经求出来了,因为矩阵要最大,所以贪心求出 l , r l,r l,r即可,那么我们就可以以这个矩阵的高度为单调性,然后每一次弹出的时候,就计算出矩阵的面积,然后开一个 A n s \mathcal{Ans} Ans记录最大值就好了.
        • 代码:
          #include <stack>
          #include <cstdio>
          using namespace std;
          
          long long Max, Hight, n;
          
          struct node {
                      //定义结构体
          	long long h, len;
          }t;
          
          int main () {
                     
          	while (scanf ("%lld", &n) != EOF && n != 0) {
                      //无限输入
          		stack <node> S; //维护一个单调不下降的栈
          		Max = -1;
          		
          		for (int i = 1; i <= n; i ++) {
                     
          			scanf ("%lld", &Hight);
          			long long Len = 0;
          			
          			while (S.empty () == false and S.top().h > Hight) {
                      //如果单调性被破坏
          				t = S.top();
          				Len += t.len; // 统计长度
          				Max = Max < t.h * Len ? t.h * Len : Max;
          				S.pop(); //弹出栈顶元素
          			}
          			
          			S.push(node{
                     Hight, Len + 1}); //更新答案
          		}
          		long long Len_ = 0;
          		
          		while (S.empty() == false) {
                      //弹出栈内元素
          			t = S.top();
          			Len_ += t.len;
          			S.pop();
          			Max = Max < t.h * Len_ ? t.h * Len_ : Max; //更新答案
          		}
          		
          		printf ("%lld\n", Max); //输出
          	}
          	return 0;
          }
          
        完美解决!!!
      • 最大连续和 (单调队列) O J \mathcal{OJ} OJ
        • 解析:由题意设 d p [ i ] dp[i] dp[i]表示到第i个数,不超过 m m m的最大连续子段和, s u m [ i ] sum[i] sum[i]表示 1 1 1 ~ i i i的前缀和.则容易得到 d p [ i ] = m a x ( s u m [ i ] − s u m [ k ] ) ( i − m < = k < = i ) dp[i] = max(sum[i] - sum[k]) (i - m <= k <= i) dp[i]=max(sum[i]sum[k])(im<=k<=i),形如此类的递推式可由单调队列维护
        • 代码:
          //我程序有个漏洞,如果输入全为负数时,输出为零
          //所以才会有特判
          #include <queue>
          #include <cstdio>
          #include <algorithm>
          
          using namespace std;
          
          const int MAXN = 200010;
          
          int n, k, f;
          deque <long long> Q; //单调队列一般使用STL中的双端队列
          long long a[MAXN], Ans = -0x3f3f3f3f;
          
          int main(
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对不起,由于我是一个文本交互的模型,我无法提供图像内容或直接链接到具体的题解或解决方案。但是,我可以帮你理解CSP-J2019公交换乘问题的基本概念和解决策略,这通常涉及到数据结构、图论以及算法设计。 CSP-J2019中的公交换乘问题可能是一个典型的旅行商问题(Traveling Salesman Problem, TSP)变种,或者是寻找最优路径的问题,其中涉及到公交网络中不同站点之间的最短路径或最少换乘次数。解决此类问题通常需要使用动态规划、贪心算法或者一些启发式搜索算法,比如A*搜索或Dijkstra算法。 如果你需要了解题目的基本思路,可能会这样操作: 1. 建立一个图,节点代表公交站点,边代表两个站点之间的路线及其长度或换乘次数。 2. 对于每个节点,计算从起点到所有其他节点的最短路径,形成一个邻接矩阵或邻接表。 3. 使用动态规划方法,例如记忆化搜索,尝试所有可能的路径,每次选择当前未访问节点中距离最近的一个,直到遍历完所有节点并回到起点,记录下总的距离或换乘次数。 4. 为了优化,可以考虑使用启发式搜索策略,如用估算的总距离作为启发信息,优先探索看起来更优的路径。 如果你对具体解法有疑问,或者想了解某个步骤的详细操作,请告诉我,我会尽力解释。至于详细的题解,建议你查阅相关的代码库、论坛帖子或在线教程,它们通常会有文字描述和步骤示例。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值