目录
一,栈
1,火车进栈
题目链接:https://www.acwing.com/problem/content/131/
题目要求输出前20种所有可能的进栈可能,并且要求是字典序最小的情况,先看图
state1表示已经出栈的火车,state2表示在栈内的火车,state3表示还没进栈的火车
对于栈内的火车,我们可以进行的操作就是将当前栈顶火车出栈,或者再进栈一个火车,因为我们要保证是字典序最小的情况,因为题目保证了火车是按从小到大的顺序进栈的,也就是说栈内的火车一定是比还没进栈的火车编号要小的,那么我们就要执行第一种操作,用深搜的方式先找出当前栈内的所有出栈可能,再进栈。
代码如下:
#include<iostream>
#include<algorithm>
#include<stack>
#include<vector>
using namespace std;
const int N=20;
vector<int>state1;//表示出栈的火车
stack<int>state2;//表示栈内的火车
int state3=1;//表示还没进栈的火车
int cnt=20,n;
void dfs()
{
if(!cnt)return;
if(state1.size()==n)//表示全部火车都已经出栈
{
cnt--;
for(int i=0;i<state1.size();i++)
cout<<state1[i];
cout<<endl;
return;
}
if(state2.size())//如果栈内有火车,就可以先执行出栈操作
{
state1.push_back(state2.top());//火车出栈
state2.pop();
dfs();
state2.push(state1.back());//恢复现场
state1.pop_back();
}
if(state3<=n)//再执行进栈操作
{
state2.push(state3);//火车进栈
state3++;
dfs();
state2.pop();//恢复现场
state3--;
}
}
int main()
{
cin>>n;
dfs();
return 0;
}
2,火车进出栈问题
这题就是一个卡特兰数的题目,把进栈的火车看成1,没进栈的火车看成0,那么用01就可以表示一个火车进出栈,用一串合法的01串就可以表示火车的所有进出栈可能,那么我们看什么样的01串才是合法的呢,那就是对于任意位置要满足1的个数不小于0的个数,这就是一个 卡特兰数
代码如下:
但是要注意的是,这题的数据范围很大,所以要用到高精度压位
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long ll;
const int mod = 1000000000, N = 120010;
int n, prime[N], v1[N], k;
ll sum[N];//sum表示第i个质因数有多少个
vector<ll>v;//高精度乘法存储答案
void get_prime(int n)//线性筛法求质数
{
for (int i = 2; i <= n; i++)
{
if (!v1[i])prime[k++] = i;
for (int j = 0; prime[j] <= n / i; j++)
{
v1[i * prime[j]] = 1;
if (i % prime[j] == 0)break;
}
}
}
ll get(int n, int p)//求n的阶乘种多有多少个p
{
ll res = 0;
while (n)
{
res += n / p;
n /= p;
}
return res;
}
vector<ll> mul(vector<ll> v, ll x)//高精度乘法,这里需要压位
{
reverse(v.begin(), v.end());
for (int i = 0; i < v.size(); i++)
v[i] *= x;
for (int i = 0; i < v.size(); i++)
{
if (v[i] < mod)continue;//压位这里就是用mod不是用10了
ll t = v[i] / mod;
v[i] %= mod;
if (i < v.size() - 1)
v[i + 1] += t;
else
v.push_back(t);
}
reverse(v.begin(), v.end());
return v;
}
int main()
{
cin >> n;
get_prime(2 * n);//线性筛质数
for (int i = 0; i < k; i++)//求质因数的个数有几个
sum[i] = get(2 * n, prime[i]) - get(n + 1, prime[i]) - get(n, prime[i]);
v.push_back(1);
for (int i = 0; i < k; i++)//高精度乘法
while(sum[i]--)
v=mul(v,prime[i]);
printf("%lld", v[0]);//第一位要先输出
for (int i=1;i<v.size();i++)
printf("%09lld", v[i]);//格式化输出,不足9位用0补齐
return 0;
}
二,队列
1,小组队列
题目链接:https://www.acwing.com/problem/content/134/
这是一道很简单的题目,因为任何时刻,同一个小组的人来到队列,他们都会站在一起,所以我们建立一个队列q0存储所有小组的编号,再为每个小组 i 建立一个队列 qi 存储队伍中这个小组的所有成员,一共n+1个队列。
当一个编号为x,组号为y的人来到队伍时,我们直接把x插入到qy末尾,如果再插入到qy末尾之前qy是空的,则还要把y插入到q0末尾,表明队伍最后出现了一个小组。
当接受到出队指令时,我们通过q0得知排在最前面的小组组号y,然后再把qy的队头出队,出队后如果qy为空,就从q0开头删除y,表明这个小组目前所有人都已经离开
代码如下:
#include<iostream>
#include<algorithm>
#include<queue>
#include<string>
using namespace std;
const int N=1010,M=1000010;
int team[M];//存储每个编号属于哪个小组
int main()
{
int t,c=1;
while(scanf("%d",&t),t)
{
queue<int>q0;//存储小组的编号
queue<int>q[N];//存储每个小组的队员编号
printf("Scenario #%d\n",c);
c++;
for(int i=0;i<t;i++)
{
int n;
scanf("%d",&n);
while(n--)
{
int x;
scanf("%d",&x);
team[x]=i;//给编号记录对应的组
}
}
string a;
while(cin>>a,a!="STOP")
{
int x;
scanf("%d",&x);
if(a=="ENQUEUE")
{
int b=team[x];//先找到这个编号对应的组
if(q[b].empty())q0.push(b);//如果这个组内是空的,说明这个组还没加入到队列,再队列后面加入这个组的编号
q[b].push(x);//将这个编号加入到它对应的组去
}
else
{
int b=q0.front();//找到队列中对头的小组编号
printf("%d\n",q[b].front());//弹出该小组的对头编号
q[b].pop();
if(q[b].empty())//如果该小组全部都出队了,就可以将该小组的编号出队列了
q0.pop();
}
}
puts(" ");
}
return 0;
}
2,双端队列
题目链接:https://www.acwing.com/problem/content/136/
这个问题很难求解,因为我们在不知道后面会有哪些数的情况,我们做出的局部决策可能会造成某个数p和q在同一个队列中,这样只要后面出现介于p和q之间的数,无论我们将这个数放到哪都会导致无解,所以我们可以先将这个序列排序成升序后,去判断我们至少需要几个双端队列
每一段序列我们需要满足什么情况才能对应原问题中的双端队列呢?我们可以依次取出排序后的所有数在原数组A中的下标,构成一个新的数组B
例如样例中的数据A=【3 6 0 9 6 3】,下标为【1 2 3 4 5 6】
排序后得到A1=【0 3 3 6 6 9】,对应的数组下标为B=【3 1 6 2 5 4】
经过分析可以发现,如果B中的一段数满足单谷性质(先单调递减,再单调递增),那么这一段就可以对应原问题的双端队列(B中保存的是下标,这一段的谷点就相当于第一个入队,递减的一段相当于从队头插入,递增的一段相当于从队尾插入)
但是还有一个要注意的是,如果A中存在几个相等的数,那么这几个数在排序时顺序时不定的,我们可以任意交换它们的顺序,是的B数组能够分成更少的段数
代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
typedef pair<int,int>pii;
const int N=200010;
pii a[N];
int main()
{
int n;
scanf("%d",&n);
for(int i=0;i<n;i++)
{
scanf("%d",&a[i].first);//存储这个数的值
a[i].second=i;//存储这个数的下标
}
sort(a,a+n);//看点的大小排好序
//res记录我们需要几个双端队列
int res=1,dir=-1,last=2e9;//dir表示递减,last记录上一个数的大小,初始化一个极大的值表示我们初始时是递减的
for(int i=0;i<n;)
{
int j=i;
while(j<n&&a[i].first==a[j].first)j++;//寻找是否有一样值得数
//minv表示连续得一段相同数中得最小下标,maxv表示最大下标,如果没有连续相同得一段数这两个数是相同得
int minv=a[i].second,maxv=a[j-1].second;
if(dir==-1)
{
if(last>maxv)//如果是下降得区间最后 一个数比最大得下标还大,说明可以继续递减
last=minv;
else//否则不行,要更改成上升
{
dir=1;
last=maxv;
}
}
else
{
if(last<minv)last=maxv;//如果是上升的,最后一个数比最小下标还小,说明可以继续上升
else//否则要更改成下降,并且我们需要开辟一个新的双端队列
{
dir=-1;
last=minv;
res++;
}
}
i=j;//这一段区间用过了就不用再用了,直接将i跳过这段区间
}
printf("%d",res);
return 0;
}
3,最大子序和
题目链接:https://www.acwing.com/problem/content/description/137/
这个题一看就可以发现需要用到前缀和+单调队列,注意一下下标和边界问题就好了
代码如下:
#include<iostream>
#include<algorithm>
#include<limits.h>
using namespace std;
const int N=300010;
int s[N],q[N];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&s[i]);
s[i]+=s[i-1];//求前缀和
}
int hh=0,tt=0,ans=-INT_MAX-1;//初始化为负无穷
for(int i=1;i<=n;i++)
{
while(hh<=tt&&i-q[hh]>m)hh++;
ans=max(ans,s[i]-s[q[hh]]);//用s[i]减去它前m个数里面的最小值来更新我们的答案
while(hh<=tt&&s[q[tt]]>=s[i])tt--;//让单调队列里面的最前面那个数为最小
q[++tt]=i;
}
printf("%d",ans);
return 0;
}
三,trie树
1,最大异或和
题目描述
在给定的 N 个整数 A1,A2……AN 中选出两个进行 xor(异或)运算,得到的结果最大是多少?
输入格式
第一行输入一个整数 N。
第二行输入 N 个整数 A1~AN。
输出格式
输出一个整数表示答案。
输入样例 复制
3 1 2 3
输出样例 复制
3
数据范围与提示
1≤N≤10^5,
0≤Ai<2^31
这题如果用两重循环暴力的话一定是会超时的,因此我们要思考如何去优化,对于一个数,我们要找到另外一个数跟他异或要最大,那么我们先来分析一下如何让两个数异或后最大
例如一个数14,他的二进制表示为1110,我们从左往右依次称第一,二,三,四位,如果想让这个数最大,那我们就要让每一位异或后为1,也就是找到一个数的这一位跟他相反,即找到0001这样的数,但是这样的数不一定存在,我们可以将已知的全部数用一个trie树存起来,之后在对每一个数去这个trie中寻找,从第一位开始,如果trie树中这一位有跟他相反的数存在,他就走到这一位,这一位异或后一定是1,所以答案就加上1<<i(i表示这一位),如果不存在,那么只能走到跟他相同的那一位去,异或后一定是0,对答案没贡献,最后把所有能异或为1的数加起来就是答案了
代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e5+10,M=3100010;//m之所以开这么大是因为树的高度是31,有1e5个数据
int a[N],son[M][2];
int idx;//给节点编号
void insert(int x)//建立字典树
{
int p=0;
for(int i=30;~i;i--)
{
int s=x>>i&1;
if(!son[p][s])
son[p][s]=++idx;//一定是要先++,因为0号节点是根节点不能再用了
p=son[p][s];
}
}
int query(int x)
{
int p=0,res=0;
for(int i=30;~i;i--)
{
int s=x>>i&1;//先判断这一位是0还是1,找到与他相反的数是否存在
if(son[p][!s])//如果存在,异或后一定是1
{
res+=1<<i;//那么对答案贡献1<<i
p=son[p][!s];
}
else
p=son[p][s];//如果不存在,那么异或后一定是0,对答案贡献0<<i==0,等于没贡献,答案为0,这里省略了没写
}
return res;
}
int main()
{
int n;
cin>>n;
for(int i=0;i<n;i++)
{
cin>>a[i];
insert(a[i]);//建立字典树
}
int res=0;
for(int i=0;i<n;i++)
res=max(res,query(a[i]));//每个数都判断一下异或的最大值
cout<<res;
return 0;
}
2,最长异或路径
题目描述
给定一个树,树上的边都具有权值。
树中一条路径的异或长度被定义为路径上所有边的权值的异或和:
⊕ 为异或符号。
给定上述的具有 n 个节点的树,你能找到异或长度最大的路径吗?
输入格式
第一行包含整数 n,表示树的节点数目。
接下来 n−1 行,每行包括三个整数 u,v,w ,表示节点 u 和节点 v 之间有一条边权重为 w 。
输出格式
输出一个整数,表示异或长度最大的路径的最大异或和。
输入样例
4 0 1 3 1 2 4 1 3 6
输出样例
7
数据范围与提示
样例中最长异或值路径应为 0->1->2,值为 7(=3⊕4)
1≤n≤100000,
0≤u,v<n,
0≤w<2^31
这题是在上题的基础上加了一些东西,我们如何把这题抽象成上题呢
我们看这个图,假设a点到根节点会经过c点,b点到根节点会经过c点,那么f(a,b)=f(root,a)^f(root,b),我们发现f(root,c)这一段会重复计算两次,那他们异或就是0,所以树上任意两点的异或值就等于这个点到根节点的异或值和另一个点到根节点的异或值,因为我们可以先预处理出来每个点到根节点的异或值,就抽象成了上一个题,题目就变成了任选两个点,找到其最大的异或值
代码如下:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e5+10,M=3100010;
int h[N],ne[N*2],e[N*2],idx,w[N*2];
int a[N],son[M][2],c;
void add(int a,int b,int x)//邻接表存储树
{
e[idx]=b;
ne[idx]=h[a];
w[idx]=x;
h[a]=idx++;
}
//father表示u节点的父节点,在搜索u的根节点时不能往回搜父节点
void dfs(int u,int father,int sum)//用一个dfs算出每个节点到根节点的异或值
{
a[u]=sum;//表示u节点到根节点的异或值
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(j!=father)
dfs(j,u,sum^w[i]);
}
}
void insert(int x)//trie树存储每个数的二进制表示
{
int p=0;
for(int i=30;i>=0;i--)
{
int s=x>>i&1;
if(!son[p][s])
son[p][s]=++c;
p=son[p][s];
}
}
int query(int x)//找每个数的最大异或值
{
int p=0,res=0;
for(int i=30;i>=0;i--)
{
int s=x>>i&1;
if(son[p][!s])
{
res+=1<<i;
p=son[p][!s];
}
else
p=son[p][s];
}
return res;
}
int main()
{
memset(h,-1,sizeof h);//邻接表头初始化
int n;
scanf("%d",&n);
for(int i=0;i<n-1;i++)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);//因为是无向图,所以要存储两边
add(v,u,w);
}
dfs(0,-1,0);//随便选一点作为根节点,这里将0号点作为根节点
for(int i=0;i<n;i++)
insert(a[i]);//建立trie树
int res=0;
for(int i=0;i<n;i++)
res=max(res,query(a[i]));
cout<<res;
return 0;
}
3,前缀统计
题目链接:https://www.acwing.com/problem/content/144/
这题就是trie树的经典应用,代码如下:
#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
const int N = 1e5 + 10, M = 1e6 + 10;
int son[M][26], idx, cnt[M];
char a[N];
void insert(char b[])
{
int p = 0;
for (int i = 0; b[i]; i++)
{
int s = b[i] - 'a';
if (!son[p][s])
son[p][s] = ++idx;
p = son[p][s];
}
cnt[p]++;//对单词的结尾进行标记
}
int query(char b[])//遍历这个单词
{
int p = 0, res = 0;
for (int i = 0; b[i]; i++)
{
int s = b[i] - 'a';
if (son[p][s])
{
p = son[p][s];
if (cnt[p])//如果这个点是一个单词的结尾,就加上这个点被标记的次数
res+=cnt[p];
}
else
break;
}
return res;
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++)
{
scanf("%s", a);
insert(a);//建立trie树
}
while (m--)
{
scanf("%s", a);
cout << query(a) << endl;
}
return 0;
}
四,STL set容器
1,邻值查找
题目链接:https://www.acwing.com/problem/content/138/
题目的大概意思就是在一个数前面找到与他数值大小最接近的数,对于这个问题,有两种做法,一种是用链表排序,判断这个点的前驱和后继哪个跟他最接近,如果用链表排序的话,我们要从最后一个点开始找,找到以后要删除这个点,然后到倒数第二个点。
第二种方法就是用STL中的set容器,本质是一个平衡树,把A1,A2,……An,依次插入到一个set集合中去,则在插入Ai之前,集合中保存的就是满足1<=j<i的所有Aj。那么再将Ai插入以后,与Ai数值大小最接近的点只会是Ai的前驱和后继,我们只要判断一下这两个数与Ai的差值,进行比较就可以了
代码如下:
#include<iostream>
#include<set>
#include<algorithm>
using namespace std;
typedef pair<int, int>pii;
const int N = 1e5 + 10;
set<pii>s;
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
pii ans;
ans.first=2e9+10;//一定要初始化为一个比较大的值,不然当这个数为第一个,并且后面的数减去x是正数的话我们就会找不到答案了
int x;
scanf("%d", &x);
s.insert({ x,i });//记录点的权值和下标
if (i > 1)
{
set<pii>::iterator it = s.find({ x,i });//找到这个数在set中的位置
if (it != s.begin())//如果这个数不是set集合中的第一个数
{
it--;//找前面一个数
ans = { x - (*it).first,(*it).second };
it++;//减了之后要还原
}
//这里要注意以下s.begin()和s.end()的区别
//如果这个数后面第一个数不是最后一个数
//就将后面的数的差与前面的数的差进行比较,如果更小就更新
if (++it != s.end() && (*it).first - x < ans.first)
ans = { (*it).first - x,(*it).second };
printf("%d %d\n", ans.first, ans.second);
}
}
return 0;
}
五,哈希
1,雪花雪花雪花
题目链接:https://www.acwing.com/problem/content/description/139/
这题也有两种做法,第一种就是用哈希和邻接表将一个雪花的六个角的和与乘积和映射在一个区间上,再通过邻接表去再映射到的区间 上的点去找是否有一样的雪花
1)哈希做法
代码如下:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
#define ll long long
using namespace std;
const int N = 1e5 + 10, P = 1e5 + 3;
int n, a[6], b[6];
struct S {
int s[6];
};
vector<S> snow[N];
int H() { //设计Hash函数
int s = 0, k = 1;
for (int i = 0; i < 6; i++) {
(s += a[i]) %= P; //和
k = (ll)k * a[i] % P; //积
}
return (s + k) % P; // 和与积相加
}
bool judge() {
for (int i = 0; i < 6; i++) {
for (int j = 0; j < 6; j++) {
bool v = 1;
for (int k = 0; k < 6; k++) {
if (a[(i + k) % 6] != b[(j + k % 6)]) {
v = 0;
break;
}
}
if (v) {
return true;
}
v = 1;
for (int k = 0; k < 6; k++) {
if (a[(i + k) % 6] != b[(j - k + 6) % 6]) {
v = 0;
break;
}
}
if (v) {
return true;
}
}
}
return false;
}
bool insert() {
int h = H(); //获取Hash值
for (unsigned int i = 0; i < snow[h].size(); i++) {
memcpy(b, snow[h][i].s, sizeof b);
if (judge()) {
return true;
}
}
S s;
memcpy(s.s, a, sizeof(s.s));
snow[h].push_back(s);
return 0;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 0; j < 6; j++) {
cin >> a[j];
}
if (insert()) {
cout << "Twin snowflakes found." << endl;
return 0;
}
}
cout << "No two snowflakes are alike." << endl;
return 0;
}
2)字符串的最小表示法做法
第二种做法就是用到一个新的知识,字符串的最小表示法, 首先介绍一下什么是字符串的最小表示法:
给定一个字符串S【1~n】,如果我们不断把它的最后一个字符放到开头,最终会得到n个字符串,称这n个字符串是循环同构的。这些字符串中字典序最小的一个,称为字符串S的最小表示
例如S=“abca”,那么它的四个循环同构字符串就是“abca",”aabc“,“caab”,“bcaa”,S的最小表示为“aabc”。与S循环同构的字符串我们可以用该字符串在S中的起始下标表示,因此我们用B[i]来表示从i开始的循环同构字符串,即S[i~n]+S[1~i-1]
接下来的问题就是如何快速的求出来字符串的最小表示
代码表示为:
int n=strlen(s+1);
for(int i=1;i<=n;i++)s[n+i]=s[i];
int i=1,j=2,k;
while(i<=n&&j<=n)
{
for(k=0;k<n&&s[i+k]==s[j+k];k++);
if(k==n)break;//形如“catcat”,它的循环元已扫描完成
if(s[i+k]>s[j+k])
{
i+=k+1;
if(i==j)i++;
}
else
{
j+=k+1;
if(i==j)j++;
}
}
ans=min(i,j);//B[ans]是最小表示
现在有了上述的知识储备,做这个题就非常好做了,只要我们将所有的雪花都表示成最小表示,然后将雪花全部排个序,如果有相同的雪花,那么这两个雪花一定是相邻的,这样判断的话时间复杂度就是O(N)的,该题代码如下:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e5+10;
int n;
int snow[6],isnow[6],b[12];
int snows[N][6],idx[N];
void get_min(int* a)//找到雪花的最小表示
{
for(int i=0;i<12;i++)
b[i]=a[i%6];
int i=0,j=1,k=0;
while(i<6&&j<6)
{
for(k=0;k<6&&a[i+k]==a[j+k];k++);
if(k==6)break;
if(b[i+k]>b[j+k])
{
i+=k+1;
if(i==j)i++;
}
else
{
j+=k+1;
if(i==j)j++;
}
}
k=min(i,j);
for(int i=0;i<6;i++)
a[i]=b[i+k];
}
bool cmp1(int snow[],int isnow[])//看顺时针的最小表示和逆时针的最小表示哪个小
{
for(int i=0;i<6;i++)
{
if(snow[i]<isnow[i])
return true;
else if(snow[i]>isnow[i])
return false;
}
return false;
}
bool cmp2(int a,int b)//给雪花的最小表示排序
{
for(int i=0;i<6;i++)
{
if(snows[a][i]<snows[b][i])
return true;
else if(snows[a][i]>snows[b][i])
return false;
}
return false;//这里如果改成true的话会tle,不知道为什么
}
int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++)
{
for(int j=0,k=5;j<6;j++,k--)
{
scanf("%d",&snow[j]);//记录顺时针旋转的雪花
isnow[k]=snow[j];//记录逆时针旋转的雪花
}
get_min(snow);//找到顺时针和逆时针的最小表示
get_min(isnow);
if(cmp1(snow,isnow))//将两者的最小表示较小的那个存进数组中
memcpy(snows[i],snow,sizeof snow);
else
memcpy(snows[i],isnow,sizeof isnow);
idx[i]=i;//给雪花编号,用于后面给雪花的最小表示排序,使得一样的雪花相邻
}
sort(idx,idx+n,cmp2);//给雪花的最小表示排序
bool flag=false;
for(int i=1;i<n;i++)
if(!cmp2(idx[i],idx[i-1])&&!cmp2(idx[i-1],idx[i]))//寻找是否有一样的雪花
flag=true;
if(flag)//表示有
puts("Twin snowflakes found.");
else//表示没有
puts("No two snowflakes are alike.");
return 0;
}
2,最长回文子串
题目链接:https://www.acwing.com/problem/content/description/141/
题目要求我们在一个字符串中找到最长的回文子串,这里考察的是二分+字符串哈希
我们可以用字符串哈希的方式处理每个前缀子串和后缀子串的哈希值,然后再进行一一比对,但是这样太慢了,会超时,所以我们可以用二分的方式去查找我们的最大长度,以每个点作为回文子串的中点,去二分枚举长度,就可以找到最长的回文子串,但是有一个问题是,如果这个回文子串是奇数,他的左右两边长度是相等的,但是如果他是偶数的话,这个回文子串的长度是不相等的,我们再这一步时要加个判断条件
要想不加判断条件的话也有个方法,那就是再每个字符之间插入一个没有在这个字符串中出现过的字符,这样无论是奇数串还是偶数串都变成了奇数串,我们就可以不用分类讨论了,可以降低代码的复杂度
代码如下:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef unsigned long long ull;
//因为要在每个字符串中间插入一个字符,所以要开两倍题目要求的大小
const int N= 2000010,P=13331;//p表示进制
ull h[N],ih[N],ph[N];
char str[N];
ull get(ull h[],int l,int r)//返回这一段区间字符串的哈希值
{
return h[r]-h[l-1]*ph[r-l+1];
}
int main()
{
int c=1;
while(scanf("%s",str+1),strcmp(str+1,"END"))
{
ph[0]=1;
int n=strlen(str+1);//计算字符串的长度
for(int i=2*n;i>0;i-=2)//在每个字符之间插入一个字符
{
str[i]=str[i/2];
str[i-1]='z'+1;//插入字符
}
n*=2;//插完之后字符串长度扩大两倍
for(int i=1,j=n;i<=n;i++,j--)
{
h[i]=h[i-1]*P+str[i];//前缀子串的哈希值
ih[i]=ih[i-1]*P+str[j];//后缀子串的哈希值
ph[i]=ph[i-1]*P;//p的i次方
}
int res=0;//记录最大答案
for(int i=1;i<=n;i++)//以每个点作为回文子串的中点进行枚举
{
int l=0,r=min(i-1,n-i);//确定左右边界,长度最长不能越界
while(l<r)//二分长度
{
int mid=l+r+1>>1;
//如果当前长度能找到回文子串就看能不能找到更大的长度
if(get(h,i-mid,i-1)==get(ih,n-(i+mid)+1,n-(i+1)+1))l=mid;
else r=mid-1;
}
if(str[i-l]<='z')res=max(res,l+1);//如果最边上的字符不是我们插进去的那个,说明回文字符长度为l+1
else res=max(res,l);//如果是的话,说明插入的字符比回文子串的数量多1,那长度就是l
}
printf("Case %d: %d\n",c++,res);
}
return 0;
}
3,后缀数组
题目链接:https://www.acwing.com/problem/content/description/142/
题目要求将后缀子串按字典序排序,然后再求相邻的后缀子串的最长公共前缀长度,根据题目要求我们可以用一个数组sa,用sa[i]来表示以str[i]开始字符子串
如果想让两个字符串子串按升序排序,我们一个一个字符对比时间复杂度是很高的,所以我们可以先找到两个字符串的最长公共前缀,然后直接比较两个字符子串的公共前缀的后一个字符就可以了,我们用二分枚举公共前缀的长度再配合字符串哈希的方式来找到两个字符字符字串的最长公共前缀长度,这样进行两个字符串比较大小的时间复杂度就是O(logN)的,这样我们就把O(N)的比较时间降到了O(logN)
代码如下:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef unsigned long long ull;
const int N=300010,P=13331;
ull h[N],ph[N];
int sa[N],n;
char str[N];
ull get(int l,int r)//返回这一段区间的哈希值
{
return h[r]-h[l-1]*ph[r-l+1];
}
int get_max_common_prefix(int a,int b)//返回以str[a]和str[b]开头的字符子串的最长公共前缀
{
int l=0,r=min(n-a+1,n-b+1);//确定最大边界
while(l<r)
{
int mid=l+r+1>>1;
if(get(a,a+mid-1)==get(b,b+mid-1))l=mid;
else r=mid-1;
}
return l;
}
bool cmp(int a,int b)
{
int l=get_max_common_prefix(a,b);//先找到以str[a]和str[b]开头的最长公共前缀
int av=a+l>n?-1:str[a+l];//判断str[a]加上最长公共前缀的长度之后是否后面还有字符,比较a和b的这两个字符
int bv=b+l>n?-1:str[b+l];//判断str[b]加上最长公共前缀的长度之后是否后面还有字符
return av<bv;//按升序排序
}
int main()
{
scanf("%s",str+1);
n=strlen(str+1);
ph[0]=1;
for(int i=1;str[i];i++)//求字符串前缀的哈希值
{
ph[i]=ph[i-1]*P;
h[i]=h[i-1]*P+str[i];
sa[i]=i;
}
sort(sa+1,sa+n+1,cmp);//让后缀子串按字典序
for(int i=1;i<=n;i++)
printf("%d ",sa[i]-1);//打印答案
printf("\n");
for(int i=1;i<=n;i++)
{
if(i>1)
printf("%d ",get_max_common_prefix(sa[i],sa[i-1]));//求每个子串与他前一个子串的最长公共前缀长度
else
printf("0 ");
}
return 0;
}
六,二叉堆
1,序列
题目链接:https://www.acwing.com/problem/content/description/148/
题目要求我们从m个序列中,在每个序列中选择一个组成一个新的序列,且序列和最小,这样的操作要重复n次,也就是要选择n个这样的序列
如果暴力做的话时间复杂度是O(N^3)的,肯定会超时,我们思考如何优化,怎么快去得找出n个这样得序列
首先假设我们有两个序列a和b,将a从小到大排序,这两个序列就是
a1,a2,a3,……,an
b1,b2,b3,……,bn
我们将所有得组合可能分组成
b1+a1, b1+a2 , b1+a1,……, b1+a1
b2+a1, b2+a2, b2+a3,……, b2+an
b3+a1, b3+a2, b3+a3, …… , b3+an
……
bn+a1, bn+a2, bn+a3, …… ,bn +an
因为我们已经将a从小到大排好序了,所以可以保证,在每一行得序列中,也是从小到大排好序得,所以对于所有组合得最小值,我们就只用在第一列中找就可以了,用一个堆可以帮助我们快速得找到最小值,对于第二个最小值,就是在第一列中减去我们刚刚找到得那个与找到的那个的后一个中找就可以得到我们的最小值,重复n次,我们就可以在两个序列中用O(NlogN)的时间复杂度找到前n个最小的组合数,然后再将这个n个数存进a中,我们就将两个序列合并成一个序列了,重复m-1次,就可以将m个序列合并成一个序列,并且序列中的值就是前n个最小的组合数,所以最后我们就将时间复杂度从O(N^3)优化到了O(MNlogN)
代码如下:
#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int,int>pii;
const int N=2010;
int n,m;
int a[N],b[N],c[N];
//a表示我们要合并到的序列,每次都把新的序列合到a中去
//b表示每次新输入的序列
//c用于临时存储合并过程中最小的n个值
void merge()
{
priority_queue<pii,vector<pii>,greater<pii>>heap;//定义一个小根堆
for(int i=0;i<n;i++)
heap.push({a[0]+b[i],0});//第一关键字存储两序列的和,第二关键字存储a的下标
for(int i=0;i<n;i++)
{
auto p=heap.top();//堆顶一定是最小的序列和,取出堆顶
heap.pop();
int p1=p.first,p2=p.second;
c[i]=p1;//将最小的放入c,最后再放回a中
heap.push({p1-a[p2]+a[p2+1],p2+1});//将最小的那个删除后,要往后移一位加入堆中
}
for(int i=0;i<n;i++)
a[i]=c[i];
}
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
scanf("%d%d",&m,&n);
for(int i=0;i<n;i++)scanf("%d",&a[i]);
sort(a,a+n);//只需要将a排序,其他的都不用排序
for(int i=0;i<m-1;i++)
{
for(int i=0;i<n;i++)
scanf("%d",&b[i]);
merge();//每输入一个新的序列都将他加到a中去,每次将两个序列的和中前n个最小的值合并成一个序列
}
for(int i=0;i<n;i++)
printf("%d ",a[i]);//全部合并后a中存储的就是答案
printf("\n");
}
return 0;
}
2,数据备份
题目链接:https://www.acwing.com/problem/content/description/149/
题目看似很简单,其实里面大有乾坤,大概意思就是选择k个区间,使得总和最小
我们很容易发现,最优解中的每两个配对的办公楼一定是相邻的。我们求出每两个相邻办公楼之间的距离,记为D1,D2,D3,D4,……,Dn-1.于是问题可以转化为:从数列D中选出不超过k个数,使他们的和最小,并且相邻的两个数不能同时被选
如果k=1,答案显然使D数列中的最小值
如果k=2,答案一定是以下两种情况之一:
1)如果选择最小值Di,以及除了Di-1,Di,Di+1之外其他数中的最小值
2)选择最小值Di左右两侧的两个数,即Di-1和Di+1
这很容易证明:如果Di-1和Di+1都没有选,那么不选最小值Di一定不是最优解;如果Di-1和Di+1选了一个,那么把选了的那个数换成Di,答案也会变小,所以最优解必定是上面两种情况之一
通过上述证明,以及进一步扩展,我们也可以得到一个结论:在最优解中,最小值左右两侧的数要么同时选,要么都不选,也就是他们是共进退的
因此我们可以得到这样一个算法,我们可以先选上D数列中的最小值,然后把Di-1,Di,Di+1从D数列中删除,把Di-1+Di+1-Di插入到D数列中刚才执行删除的位置。最后,求解”从新的D数列中选出不超过k-1个数,使它们的和最小,并且相邻两个数不能同时选“这个子问题
在这个子问题中,如果选了Di-1+Di+1-Di这个数,相当于去掉Di,换上Di-1和Di+1;如果没选,那么刚才选择最小值显然是一步最优策略,这恰好涵盖了我们在推论中的所有情况
我们可以通过双链表和set容器来实现我们的思路,代码如下:
#include<iostream>
#include<algorithm>
#include<set>
using namespace std;
typedef long long ll;
typedef pair<ll,int>pli;
const int N=100010;
int n,k;
ll d[N];//表示两两楼之间的距离
int l[N],r[N];//双链表左指针和右指针
void delete_node(int p)//删除这个节点
{
l[r[p]]=l[p];
r[l[p]]=r[p];
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)scanf("%lld",&d[i]);//读入信息
for(int i=n;i>1;i--)d[i]-=d[i-1];//求出两两楼之间的信息
set<pli>s;
d[1]=d[n+1]=1e15;//注意边界问题,将两边初始化成极大值,防止最边上的端点被选后他后面那个端点又被选
for(int i=2;i<=n;i++)
{
s.insert({d[i],i});//插入节点数值和编号,用于找他的前驱和后继
r[i]=i+1;//记录该节点的后继
l[i]=i-1;//记录该节点的前驱
}
ll res=0;//记录答案,可能会爆int
while(k--)
{
auto it=s.begin();//取出set中的队首,即为最小值
ll v=it->first;
int p=it->second,left=l[p],right=r[p];//找到这个最小值的前驱和后继
s.erase(it);//在集合中删除这个最小值的点
s.erase({d[left],left});//在集合中删除这个点的前驱
s.erase({d[right],right});//在集合中删除这个点的后继
delete_node(left),delete_node(right);//同时要在双链表中删除这个最小值的点的前驱和后继
//本来是要在双链表中把最小值的那个点也删去的,但是后面我们还要插入一个新的点,所以我们就可以直接把这个点的信息
//修改成我们要插入的那个点的信息,相当于进行了依次删除和插入操作,简化了代码
res+=v;//答案加上这个最小值
d[p]=d[left]+d[right]-d[p];//新建一个节点信息,也等于修改这个最小值节点的信息,改成两条边的和减去选择的这条边
s.insert({d[p],p});//将新建的点重新插入到集合中
}
cout<<res;
return 0;
}
七,KMP
1,周期
题目链接:141. 周期 - AcWing题库
这题是要求前缀子串的完美循环节以及完美循环节的长度,直接给出结论,证明在acwing的上面有视频讲解
结论:如果一个以下标 i 结尾的字符串有循环节,那么这个最短的循环节一定是 i - next[i],如果这个字符串是完美循环的话同时要满足(i- next[i])能整除 i ,出现的次数就是 i/(i-next[i]),并且当(i- next[i])能整除 i 时,这个字符串的所有循环节一定是最短循环节的倍数 ,如果(i- next[i])不能整除 i ,说明这个字符串不是完美循环的,没有完美循环节,但还是有最小循环节的,只是这个最小循环节不能完美覆盖这个字符串,但是能覆盖掉这个字符串。
代码如下:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1000010;
int ne[N];
char a[N];
int main()
{
int n,t=1;
while(scanf("%d",&n),n)
{
scanf("%s",a+1);
ne[1]=0;
for(int i=2,j=0;i<=n;i++)//求next数组
{
while(j&&a[i]!=a[j+1])j=ne[j];
if(a[i]==a[j+1])j++;
ne[i]=j;
}
printf("Test case #%d\n",t++);
for(int i=2;i<=n;i++)//题目要求前缀的长度要大于1,所以i从2开始
{
int t=i-ne[i];
if(i%t==0&&i/t>1)//循环节的长度大于1且能整除i,说明1~i的这个前缀子串有循环节,k=i/t
printf("%d %d\n",i,i/t);
}
puts(" ");
}
return 0;
}
2,奶牛矩阵
题目链接:https://www.acwing.com/problem/content/description/161/
这个题目跟上一个题目有很强的联系,都运用了kmp的性质,求最小的循环节,上面已经讲了,如果想要字符串被他的最小循环节完美覆盖,那么这个循环节的长度要能整除字符串的长度,并且只有满足这个条件,该字符串的所有循环节才是最小循环节的倍数,如果不能整除的话,就不能保证所有循环节是最小循环节的倍数,这个题并没有保证每行的字符串都被他的循环节完美覆盖,所以这个题我们不能求每一行的最小循环节来取最小公倍数或者求最大值作为子矩阵的宽度,因为宽度一旦改变就不能保证这个宽度是每一行字符串的循环节宽度
解题思路:看到列数的数据范围很小,所以这题,我们可以暴力枚举循环节的宽度,找到最短的那个宽度width,满足所有行的字符串都能被以这个宽度的循环节覆盖,找到以后把每一行的长度为width的字符串看成一个字母,在列上求kmp 的next数组,云以运用kmp的性质,求列上最小循环节的长度,作为子矩阵的高度
代码如下:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e4+10,M=80;
char a[N][M];//题意字符串
int ne[N];//列上的KMP的next数组
bool st[M];//判读合法的子矩阵宽度
int main()
{
memset(st,true,sizeof st);//先全部初始化为true,只要有一行的宽度不满足条件的就置为false
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%s",a[i]);//读入字符串
for(int j=1;j<=m;j++)//枚举子矩阵的宽度
{
bool is_match=true;
for(int k=j;k<m;k+=j)//枚举该行字符串的宽度为j的每一个部分,第二第三第四……部分
{
for(int u=0;u<j&&k+u<m;u++)//始终用第一部分与后面的每一部分去匹配
{
if(a[i][u]!=a[i][k+u])//如果有不匹配的,说明该长度不能作为这个字符串的循环节的长度
{
is_match=false;
break;
}
}
if(!is_match)
break;
}
if(!is_match)//只要有一行的宽度不匹配就置为false
st[j]=false;
}
}
int width=0;//记录最小宽度
for(int i=1;i<=m;i++)
{
if(st[i])//表示全部行都枚举完了,这个宽度是所有行的循环节的长度,从小到大枚举找到最小的宽度
{
width=i;
for(int i=1;i<=n;i++)//找到最小宽度以后要将宽度为width的字符串后面补0,方面后面列上的比较
a[i][width]='\0';
break;
}
}
for(int i=2,j=0;i<=n;i++)//列上的kmp,把每一行宽度为width的字符串看成一个字符
{
while(j&&strcmp(a[i],a[j+1]))j=ne[j];
if(strcmp(a[i],a[j+1])==0)j++;
ne[i]=j;
}
int height=n-ne[n];//运用了kmp的性质,求最小循环节的长度,也就是子矩阵的高度
printf("%d",width*height);
return 0;
}
八,习题
1,矩阵
题目链接:https://www.acwing.com/problem/content/158/
本题考察二维字符串的哈希,我们如何用将二维字符串进行哈希呢,假设有一个a=3,b=3的3*3矩阵
abcdefghi表示矩阵中该位的数值, 我们把第一行的数看成最高位,第二行次之,第三行看成最低位,那么这个矩阵的哈希值就是a*(P^8)+b*(P^7)+c*(P^6)+……I*(P^0),P表示进制,这样求表示一个矩阵的哈希值
我们首先求出每一行的哈希值,然后求出长度为b的区间的哈希值,当想要求往下移一行的矩阵的哈希值的时,就可以直接当前矩阵的哈希值*(P^b)+下一行长度为b的区间的哈希值-第一行长度区间为b的哈希值*(P^(a*b)),这样就可以快速求出N*M的矩阵中的所有a*b的子矩阵的哈希值,对于后面的询问,只用求出来每个询问的矩阵的哈希值,判断在N*M的矩阵中有没有对应出现的就可以了
代码如下:
#include<iostream>
#include<algorithm>
#include<unordered_set>
using namespace std;
typedef unsigned long long ull;
const int N=1010,P=13331;
int n,m,a,b;
ull h[N][N],ph[N*N];//因为是二维,ph要开N*N倍空间
char str[N];//表示每一行的字符串
ull get(ull h[],int l,int r)//求这一行的这一段区间的哈希值
{
return h[r]-h[l-1]*ph[r-l+1];
}
int main()
{
unordered_set<ull>S;//存储大矩阵中的所有子矩阵的哈希值
scanf("%d%d%d%d",&n,&m,&a,&b);
ph[0]=1;
for(int i=1;i<=n*m;i++)//求p的n次方
ph[i]=ph[i-1]*P;
for(int i=1;i<=n;i++)
{
scanf("%s",str+1);//输入每一行的字符
for(int j=1;j<=m;j++)
h[i][j]=h[i][j-1]*P+str[j];//求出这一行所有前缀的哈希值
}
for(int i=b;i<=m;i++)//先枚举列,因为子矩阵是从a*b大小的,宽度最短是b
{
int l=i-b+1,r=i;
ull s=0;
for(int j=1;j<=n;j++)//再枚举行,这里行从1开始是因为,我们是递推的求出来每个子矩阵的大小的
{
s=s*ph[b]+get(h[j],l,r);//求子矩阵的哈希值
if(j>a)s-=get(h[j-a],l,r)*ph[a*b];//说明矩阵大小超过了a*b,继续往下走,要减去该子矩阵最上面那一行的哈希值
if(j>=a)S.insert(s);//只有满足了a*b大小的子矩阵我们才会记录下来
}
}
int q;
scanf("%d",&q);//q个询问
while(q--)
{
ull s=0;
for(int i=1;i<=a;i++)
{
scanf("%s",str+1);
for(int j=1;j<=b;j++)
s=s*P+str[j];//求子矩阵的哈希值
}
if(S.count(s))//在大矩阵中出现过
puts("1");
else//没出现过
puts("0");
}
return 0;
}
2,电话列表
题目链接:https://www.acwing.com/problem/content/description/163/
这题要保证我们在插入trie树的过程中当前串既不是其他串的前缀,也要保证其他串不是当前串的前缀,考虑其他串是否是当前串的前缀,只要我们在插入遍历的过程中看下是否有被标记过的点,如果有就表示有其他串是当前串的前缀,考虑当前串是否是其他串的前缀,只需要我们在插入的过程中看是否新建了新的节点,如果新建了,表示当前串不是其他串的前缀
代码如下:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=10010;
int son[N*10][10],idx,cnt[N*10];
char str[15];
bool add(char a[])
{
bool has_new=false;
bool has_match=false;
int p=0;
for(int i=0;a[i];i++)
{
int u=a[i]-'0';
if(!son[p][u])
{
son[p][u]=++idx;
has_new=true;
}
p=son[p][u];
if(cnt[p])
has_match=true;
}
cnt[p]++;
return has_new&&!has_match;//创建了新节点并且没有跟之前的的字符匹配,表示没问题,该列表是兼容的
}
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
idx=0;
memset(son,0,sizeof son);
memset(cnt,0,sizeof cnt);
int n;
scanf("%d",&n);
bool flag=true;
while(n--)
{
scanf("%s",str);
if(!add(str))//如果不是兼容的,就返回的是false,说明这个列表不是兼容的
flag=false;
}
if(flag)
puts("YES");
else
puts("NO");
}
return 0;
}
3,黑盒子
题目链接:https://www.acwing.com/problem/content/description/164/
这题跟动态中位数那题很像,同样是用对顶堆,只不过动态中位数那题保证小根堆的堆顶一定是中位数,但是这题我们小根堆的堆顶不一定是中位数,是我们需要的第k位数
代码如下:
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
const int N=30010;
int n,m;
int a[N],b[N];
int main()
{
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++)scanf("%d",&a[i]);
for(int i=0;i<m;i++)scanf("%d",&b[i]);
priority_queue<int>left;//左边为大根堆,
priority_queue<int,vector<int>,greater<int>>right;//右边为小根堆,保证每次小根堆的堆顶为我们需要的答案
int i=0,j=0;
while(i<n||j<m)
{
while(b[j]==i)
{
printf("%d\n",right.top());
left.push(right.top());
right.pop();
j++;
}
left.push(a[i]);//每次将新的元素往左边插入
if(left.size()>=j)//但是一定保证左边的数一直要小于j,这样才能保证右边的堆堆顶是我们想要的答案
{
right.push(left.top());
left.pop();
}
i++;
}
return 0;
}
4,匹配矩阵
题目链接:https://www.acwing.com/problem/content/description/162/
这个题可以用字符串哈希做也可以用kmp做,但是kmp那个做法我还没弄懂,所以先用字符串哈希做,字符串哈希的做法相较于kmp的做法更简单,思路更好想
我们首先把字符串a和b的每个前缀子串的哈希值求出来,就可以在O(1)的时间求出来其中任何一段子串的哈希值,我们枚举a的每个一个字符作为后缀子串的开头,然后二分长度,找到最大的与字符串b匹配的长度
代码如下:
#include<iostream>
#include<algorithm>
#include<unordered_map>
using namespace std;
typedef unsigned long long ull;
const int N=200010,P=13331;
int n,m,q;
ull ha[N],ph[N],hb[N];
char a[N],b[N];
unordered_map<int,int>mp;
ull get(ull h[],int l,int r)
{
return h[r]-h[l-1]*ph[r-l+1];
}
int main()
{
scanf("%d%d%d",&n,&m,&q);
ph[0]=1;
scanf("%s%s",a+1,b+1);
for(int i=1;i<=n;i++)//求a字符串的哈希值
{
ph[i]=ph[i-1]*P;
ha[i]=ha[i-1]*P+a[i];
}
for(int i=1;i<=m;i++)//求b字符串的哈希值
hb[i]=hb[i-1]*P+b[i];
for(int i=1;i<=n;i++)//枚举以a[i]开头的字符串
{
int l=0,r=n-i+1;
while(l<r)//二分枚举长度
{
int mid=l+r+1>>1;
if(get(hb,1,1+mid-1)==get(ha,i,i+mid-1))l=mid;//找到以a[i]开头的字符串和b的前缀子串的最长匹配长度
else r=mid-1;
}
mp[l]++;//找到的长度记录下来
}
while(q--)
{
int x;
scanf("%d",&x);
printf("%d\n",mp[x]);
}
return 0;
}