括号画家
达达是一名漫画家,她有一个奇特的爱好,就是在纸上画括号。
这一天,刚刚起床的达达画了一排括号序列,其中包含小括号( )、中括号[ ]和大括号{ },总长度为N。
这排随意绘制的括号序列显得杂乱无章,于是达达定义了什么样的括号序列是美观的:
(1) 空的括号序列是美观的;
(2) 若括号序列A是美观的,则括号序列 (A)、[A]、{A} 也是美观的;
(3) 若括号序列A、B都是美观的,则括号序列AB也是美观的。
例如 (){} 是美观的括号序列,而)({)[}]( 则不是。
现在达达想在她绘制的括号序列中,找出其中连续的一段,满足这段子序列是美观的,并且长度尽量大。
你能帮帮她吗?
建立一个hash表,作为左括号到匹配的右括号的映射。利用栈来存放括号,如果栈不为空,且当前括号是左括号,其hash映射右括号等于栈顶右括号,那么我们将栈顶弹出,并建立一个flag数组,将两个括号所在位置对应的flag置为1。其他情况则一律放入栈中。
最终我们遍历flag中连续的1的长度,最长的长度就是我们想要的答案。
#include <iostream>
#include <stack>
#include <string>
#include <map>
using namespace std;
int res;
int flag[100010];
int main(){
// first是元素,second是索引
stack<pair<char, int>> s, temp;
string str;
// hash映射 左括号到右括号
map<char, char> hash = {{'(', ')'}, {'[', ']'}, {'{', '}'}};
cin >> str;
for(int i=0; i<str.length(); i++){
// 如果是左括号,则push
if(str[i] == '(' || str[i] == '[' || str[i] == '{') s.push({str[i],i});
// 如果栈不为空,且栈顶元素的hash映射等于当前元素,则弹出,并将两个括号所在位置对应的flag置为1
else if(s.size() && str[i] == hash[s.top().first]){
flag[s.top().second] = 1;
flag[i] = 1;
s.pop();
}
// 其余情况push
else s.push({str[i], i});
}
// 遍历flag
for(int i=0; i<str.length(); i++){
int ans = 0;
while(flag[i] == 1){
ans++;
i++;
}
res = max(ans, res);
}
cout << res << endl;
return 0;
}
表达式计算
给出一个表达式,其中运算符仅包含+,-,*,/,^(加 减 乘 整除 乘方)要求求出表达式的最终值。
数据可能会出现括号情况,还有可能出现多余括号情况。
数据保证不会出现大于或等于231的答案。
数据可能会出现负数情况。
表达式的计算可以类比成树,例如我们在计算22+33的时候,会生成如下形式的树:
而和树结构有关的题,都很大可能性要用到栈,因此我们用两个栈来实现表达式计算。一个栈来存放数字,一个栈来存放运算符。当当前运算符的优先级低于栈顶优先级的时候,我们可以运算栈顶。当出现括号的时候,我们就要把括号里的运算全部完成。当出现负数的时候,我们就在其前面补上0,变成减法运算。
#include <iostream>
#include <algorithm>
#include <stack>
#include <string>
#include <math.h>
using namespace std;
// num存储数值,opt存储运算符
stack<long long> num;
stack<char> opt;
// 计算函数
void cal() {
if(opt.empty()) return;
char sig = opt.top();
opt.pop();
long long a = num.top();
num.pop();
long long b = num.top();
num.pop();
if(sig == '+') num.push(a+b);
else if(sig == '-') num.push(b - a);
else if(sig == '*') num.push(a * b);
else if(sig == '/') num.push(b / a);
else if(sig == '^') num.push((long long)pow(b, a));
}
int main(){
string str;
cin >> str;
// 补充左括号
string pre(str.length(), '(');
str = pre + str;
str += ')';
for(int i=0; i<str.length(); i++) {
// 如果当前运算符是+/-
if(str[i] == '+' || str[i] == '-') {
// 如果当前运算符的前一位是左括号,则证明这是个负数,我们数值补上0,运算符补上-
if(i && str[i - 1] == '(') {
num.push(0);
opt.push('-');
}
// 否则的话只要+/-之前不是括号,我们都要计算前面的运算
else {
if(opt.top() != '(') cal();
opt.push(str[i]);
}
}
// 如果是*//,那么只要前面的运算符不比当前低都要运算
else if(str[i] == '*' || str[i] == '/') {
if(opt.top() != '(' && opt.top() != '+' && opt.top() != '-') cal();
opt.push(str[i]);
}
// 如果是^,那么只有是同级才运算
else if(str[i] == '^') {
if(opt.top() == '^') cal();
opt.push(str[i]);
}
// 如果是左括号,都要push
else if(str[i] == '(') opt.push(str[i]);
// 如果是右括号,我们要循环运算直到左括号
else if(str[i] == ')') {
while(opt.top() != '(') cal();
opt.pop();
}
// 如果是数字就push
else if(str[i] >= '0' && str[i] <= '9') {
string temp;
while(i<str.length() && str[i] >= '0' && str[i] <= '9') {
temp += str[i++];
}
--i;
num.push((long long)stoi(temp));
}
}
cout << num.top() << endl;
return 0;
}
城市游戏
有一天,小猫rainbow和freda来到了湘西张家界的天门山玉蟾宫,玉蟾宫宫主蓝兔盛情地款待了它们,并赐予它们一片土地。
这片土地被分成NM个格子,每个格子里写着’R’或者’F’,R代表这块土地被赐予了rainbow,F代表这块土地被赐予了freda。
现在freda要在这里卖萌。。。它要找一块矩形土地,要求这片土地都标着’F’并且面积最大。
但是rainbow和freda的OI水平都弱爆了,找不出这块土地,而蓝兔也想看freda卖萌(她显然是不会编程的……),所以它们决定,如果你找到的土地面积为S,它们将给你3S两银子。
参考栈中直方图中最大矩形。
#include <iostream>
#include <algorithm>
#include <stack>
using namespace std;
const int N = 1010;
// r,c代表矩阵的长宽,land存储矩阵,h存储每行每列F的高度
int r, c;
char land[N][N];
int h[N][N];
// 计算每一行的最大F面积
int cal(int* nums) {
// 单调栈,递增,first存放高度,second存放索引
stack<pair<int, int>> s;
int res = 0;
for(int i=0; i<c; i++) {
while(s.size() && s.top().first > nums[i]) {
int num = s.top().first;
s.pop();
// l存放弹出元素的左边界,length代表当前height的长度,area是面积
int l = s.size()? s.top().second: -1;
int length = i - l - 1;
int area = length * num;
res = max(res, area);
}
s.push({nums[i], i});
}
// 最后处理单调栈中剩余元素
while(s.size()) {
int num = s.top().first;
s.pop();
int l = s.size()? s.top().second: -1;
int length = c - l - 1;
int area = length * num;
res = max(res, area);
}
// cout << res << endl;
return res;
}
int main(){
cin >> r >> c;
for(int i=0; i<r; i++) {
for(int j=0; j<c; j++) {
cin >> land[i][j];
}
}
// 形成h矩阵
for(int j=0; j<c; j++) {
for(int i=0; i<r; i++) {
if(land[i][j] == 'F') {
h[i][j] = i == 0? 1: 1 + h[i-1][j];
}
else {
h[i][j] = 0;
}
}
}
int res = 0;
for(int i=0; i<r; i++) {
res = max(res, cal(h[i]));
}
cout << 3 * res << endl;
return 0;
}
双栈排序
Tom最近在研究一个有趣的排序问题。
通过2个栈S1和S2,Tom希望借助以下4种操作实现将输入序列升序排序。
操作a
如果输入序列不为空,将第一个元素压入栈S1
操作b
如果栈S1不为空,将S1栈顶元素弹出至输出序列
操作c
如果输入序列不为空,将第一个元素压入栈S2
操作d
如果栈S2不为空,将S2栈顶元素弹出至输出序列
如果一个1~n的排列P可以通过一系列操作使得输出序列为1, 2,…,(n-1), n,Tom就称P是一个”可双栈排序排列”。
例如(1, 3, 2, 4)就是一个”可双栈排序序列”,而(2, 3, 4, 1)不是。
Tom希望知道其中字典序最小的操作序列是什么。
两个数字不能放到一个栈中的条件就是:
存在i < j < k, 如果num[k] < num[i] < num[j], 则这i和j不能放入一个栈中。
如果这组数可以被双栈排序,那么在每冲突的两个节点i和j之间连上一条边,必定会形成一个二分图。因此我们判断的标准就是能否形成二分图即可,利用深度优先搜索,对树进行染色。
而最小字典序,我们只需要按照数组顺序,在满足条件的情况下,优先放入第一个栈,即可得到最小字典序。
#include <iostream>
#include <algorithm>
#include <stack>
#include <limits.h>
#include <string.h>
using namespace std;
const int N = 1010;
// color表示每个节点所染的颜色,graph存放二分图,通过染色判断是否可分
int num[N], low[N], color[N], graph[N][N];
int n;
stack<int> s1, s2;
// 深度优先搜索对二分图染色 i表示节点,c表示颜色
bool dfs(int i, int c) {
// 将当前节点i染色c
color[i] = c;
// 遍历所有节点
for(int j=0; j<n; j++) {
// 如果和j节点有连边
if(graph[i][j]) {
// 如果当前节点j未染色 并且dfs返回的合理的,那么就继续遍历,或者j已染色,无冲突,也继续遍历。否则的话就返回false
if((color[j] == -1 && dfs(j, !c)) || color[j] != c) continue;
else return false;
}
}
return true;
}
int main(){
cin >> n;
for(int i=0; i<n; i++) cin >> num[i];
// low存放索引i以后(包含i)的最小值
low[n] = INT_MAX;
for(int i=n-1; ~i; i--) low[i] = min(low[i+1], num[i]);
// 如果满足上述条件,说明不能放到一个栈中,就将两点相连
for(int i=0; i<n-1; i++){
for(int j=i+1; j<n; j++){
if(num[i] < num[j] && low[j+1] < num[i]) graph[i][j] = graph[j][i] = 1;
}
}
// color -1代表未染色,0和1代表两种染色
memset(color, -1, sizeof(color));
for(int i=0; i<n; i++) {
if(color[i] == -1){
// 如果没染色 dfs一下合理,遍历下一个
if(dfs(i, 0)) continue;
else {
cout << 0 << endl;
return 0;
}
}
}
// index表示当前遍历的数字,i表示索引
int index = 1, i = 0;
while(!i || s1.size() || s2.size()) {
// 如果当前栈顶等于遍历的数字,则弹出
while(1) {
if(s1.size() && s1.top() == index) {
s1.pop();
cout << "b ";
}
else if(s2.size() && s2.top() == index) {
s2.pop();
cout << "d ";
}
else break;
++index;
}
// 否则的话压栈
if(i < n && color[i] == 0) {
s1.push(num[i++]);
cout << "a ";
}
else if(i < n) {
s2.push(num[i++]);
cout << "c ";
}
}
cout << endl;
return 0;
}
滑动窗口
给定一个大小为n≤106的数组。
有一个大小为k的滑动窗口,它从数组的最左边移动到最右边。
您只能在窗口中看到k个数字。
每次滑动窗口向右移动一个位置。
您的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
单调队列,easy…
#include <iostream>
#include <algorithm>
#include <deque>
#include <vector>
using namespace std;
const int N = 1000010;
int n, k;
int num[N];
vector<int> res1, res2;
deque<pair<int, int>> a, b;
int main(){
cin >> n >> k;
for(int i=0; i<n; i++) {
cin >> num[i];
}
for(int i=0; i<n; i++) {
while(a.size() && a.back().first > num[i]) {
a.pop_back();
}
a.push_back({num[i], i});
if(a.front().second < i - k + 1) a.pop_front();
if(i >= k - 1) res1.push_back(a.front().first);
while(b.size() && b.back().first < num[i]) {
b.pop_back();
}
b.push_back({num[i], i});
if(b.front().second < i - k + 1) b.pop_front();
if(i >= k - 1) res2.push_back(b.front().first);
}
for(int val : res1) cout << val << " ";
cout << endl;
for(int val : res2) cout << val << " ";
cout << endl;
return 0;
}
内存分配
内存是计算机重要的资源之一,程序运行的过程中必须对内存进行分配。
经典的内存分配过程是这样进行的:
1、 内存以内存单元为基本单位,每个内存单元用一个固定的整数作为标识,称为地址。地址从0开始连续排列,地址相邻的内存单元被认为是逻辑上连续的。我们把从地址i开始的s个连续的内存单元称为首地址为i长度为s的地址片。
2、 运行过程中有若干进程需要占用内存,对于每个进程有一个申请时刻T,需要内存单元数M及运行时间P。在运行时间P内(即T时刻开始,T+P时刻结束),这M个被占用的内存单元不能再被其他进程使用。
3、假设在T时刻有一个进程申请M个单元,且运行时间为P,则:
(1)若T时刻内存中存在长度为M的空闲地址片,则系统将这M个空闲单元分配给该进程。若存在多个长度为M个空闲地址片,则系统将首地址最小的那个空闲地址片分配给该进程。
(2)如果T时刻不存在长度为M的空闲地址片,则该进程被放入一个等待队列。对于处于等待队列队头的进程,只要在任一时刻,存在长度为M的空闲地址片,系统马上将该进程取出队列,并为它分配内存单元。注意,在进行内存分配处理过程中,处于等待队列队头的进程的处理优先级最高,队列中的其它进程不能先于队头进程被处理。
现在给出一系列描述进程的数据,请编写一程序模拟系统分配内存的过程。
输出包括2行。
第一行是全部进程都运行完毕的时刻。
第二行是被放入过等待队列的进程总数。
#include <iostream>
#include <algorithm>
#include <queue>
#include <vector>
#include <set>
#include <limits.h>
using namespace std;
// cnt放入过等待队列的进程总数,申请时刻T,需要内存单元数M及运行时间P
int n, cnt;
int t, m, p;
// use代表正在进行中的进程,wt代表等待队列,ts小顶堆存放即将结束的进程
set<pair<int, int>> use;
queue<pair<int, int>> wt;
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> ts;
// exist来判断是否还有位置运行程序,如果有,就存放进use,把结束时间也加入ts
bool exist() {
for(auto iter = use.begin(); iter != use.end(); iter++) {
auto last = iter;
last++;
if(last == use.end() && n - iter->first - iter->second >= m || last != use.end() && last->first - iter->first - iter->second >= m) {
use.insert({iter->second + iter->first, m});
ts.push({t + p, iter->second + iter->first});
return true;
}
}
return false;
}
// run返回所有进程结束的时间,同时判断当前是否可以将wt队列中的进程加入到use
int run(int time) {
// final记录ts堆顶进程结束的时间
int final = 0;
while(ts.size() && ts.top().first <= time) {
final = ts.top().first;
// cout << final << endl;
while (ts.size() && ts.top().first == final) {
auto iter = use.lower_bound({ts.top().second, 0});
use.erase(iter);
ts.pop();
}
// flag判断是否wt队列队头是否弹出,弹出的话就继续循环,没有弹出就结束
bool flag = true;
while (wt.size() && flag) {
int mem = wt.front().first;
int over = wt.front().second;
flag = false;
// 判断队列队首进程是否可以use,可以就继续循环,不可以就跳出循环
for (auto iter = use.begin(); iter != use.end(); iter++) {
auto last = iter;
last++;
if (last == use.end() && n - iter->first - iter->second >= mem || last != use.end() && last->first - iter->first - iter->second >= mem) {
use.insert({iter->second + iter->first, mem});
ts.push({final + over, iter->second + iter->first});
wt.pop();
flag = true;
break;
}
}
}
}
return final;
}
int main(){
cin >> n;
int res = 0;
use.insert({-1, 1});
// 判断是否结束,结束就break。先判断是够有结束的进程,并判断wt队列是否有进程可以加入。判断当前的进程是否可以加入use,不能的话放入等待队列wt。
while(true) {
cin >> t >> m >> p;
if(t == 0 && m == 0 && p == 0) break;
res = max(res, run(t));
if(!exist()) {
wt.push({m, p});
cnt++;
}
}
while(ts.size()) res = max(res, run(INT_MAX));
cout << res << endl;
cout << cnt << endl;
return 0;
}
矩阵
给定一个M行N列的01矩阵(只包含数字0或1的矩阵),再执行Q次询问,每次询问给出一个A行B列的01矩阵,求该矩阵是否在原矩阵中出现过。
可以利用hash的方式,为矩阵的每一子块计算hash值,可以实现O(1)查询。
快速计算矩阵hash值,可以先为矩阵每一行计算hash。然后设计a*b的滑动窗口,逐行逐列滑动,新加入的行加入hash值中,去掉滑出行的hash值。
对于矩阵的hash值可以如下图:
#include <iostream>
#include <algorithm>
#include <string>
#include <unordered_set>
using namespace std;
const int N = 1010, P = 131;
// a,b代表查询字块大小,m,n代表矩阵大小,q代表查询次数,matrix存放矩阵,grid存放查询子块,p存放131倍数,grid存放
int a, b, m, n, q;
unsigned int matrix[N][N];
unsigned int grid[N][N];
unsigned int p[N*N];
unordered_set<unsigned int> s;
// 计算一行一部门的hash值
unsigned int get(int row, int l, int r) {
return matrix[row][r] - matrix[row][l] * p[r - l];
}
int main(){
cin >> m >> n >> a >> b;
p[0] = 1;
for(int i=1; i<=a*b; i++) p[i]= p[i-1] * P;
// 对矩阵每一行计算hash
string str;
for(int i=1; i<=m; i++) {
cin >> str;
for(int j=1; j<=n; j++) {
matrix[i][j] = str[j-1] - '0' + matrix[i][j-1] * P;
}
}
// 计算矩阵每一ab块的hash值,并存入set中
unsigned int hash = 0;
for(int j=b; j<=n; j++) {
hash = 0;
for(int i=0; i<=m; i++) {
hash = hash * p[b] + get(i, j - b, j);
if(i < a) continue;
hash -= get(i-a, j - b, j) * p[a * b];
s.insert(hash);
}
}
// 查询,先计算查询字块hash值,再去set中匹配
cin >> q;
while(q--) {
unsigned int query = 0;
for(int i=1; i<=a; i++) {
cin >> str;
for(int j=1; j<=b; j++) {
grid[i][j] = str[j-1] - '0' + grid[i][j-1] * P;
}
query = query * p[b] + grid[i][b];
}
if(s.find(query) == s.end()) cout << 0 << endl;
else cout << 1 << endl;
}
return 0;
}
树形地铁系统
一些主要城市拥有树形的地铁系统,即在任何一对车站之间,有且只有一种方式可以乘坐地铁。
此外,这些城市大多数都有一个中央车站。
想象一下,你是一名在拥有树形地铁系统的城市游玩的游客,你想探索该城市完整的地铁线路。
你从中央车站出发,随机选择一条地铁线,然后乘坐地铁行进。
每次到达一个车站,你都将选择一条尚未乘坐过的地铁线路进行乘坐。
如果不存在未乘坐过的线路,则退回到上一个车站,再做选择。
直到你将所有地铁线路都乘坐过两次(往返各一次),此时你将回到中央车站。
之后,你以一种特殊的方式回忆自己的坐车过程,你将你的完整地铁乘坐路线编码为一个二进制字符串。
其中0编码表示你乘坐地铁线路到达距离中央车站更远的一站,1编码表示你乘坐地铁线路到达距离中央车站更近的一站。
如果两个字符串描述的探索路线可以视为同一个地铁系统的两种探索路线,则输出same。
否则,输出different。
该题可以转换为树的最小表示,如果两个树结构是相同的,那么他们有唯一的最小表示,是以0表示向叶节点靠近,1表示向根节点靠近的字符串。而树的最小表示就是子树的最小表示排序处理得到的,因此我们可以递归的算出树的最小表示,再判断两个树是否相同。
#include <iostream>
#include <algorithm>
#include <string>
#include <vector>
using namespace std;
int n, pos;
// 求解树的最小表示
string represent(string s) {
// res代表以当前节点作为根节点得到的子树的最小表示结果
vector<string> res;
pos++;
// 如果递归未结束,且当前节点下还有节点则继续递归
while(pos < s.length() && s[pos] == '0') {
res.push_back(represent(s));
++ pos;
}
// 将得到的子树最小表示排序,再合并就是最终结果
sort(res.begin(), res.end());
string ans;
for(string str : res) ans += str;
ans = '0' + ans + '1';
return ans;
}
int main(){
cin >> n;
while(n--) {
string s1, s2;
cin >> s1 >> s2;
s1 = '0' + s1 + '1';
s2 = '0' + s2 + '1';
pos = 0;
string dicsort1 = represent(s1);
pos = 0;
string dicsort2 = represent(s2);
if(dicsort1 == dicsort2) cout << "same" << endl;
else cout << "different" << endl;
}
return 0;
}
项链
有一天,达达捡了一条价值连城的宝石项链,但是,一个严重的问题是,他并不知道项链的主人是谁!
在得知此事后,很多人向达达发来了很多邮件,都说项链是自己的,要求他归还(显然其中最多只有一个人说了真话)。
达达要求每个人都写了一段关于自己项链的描述: 项链上的宝石用数字0至9来标示。
一个对于项链的表示就是从项链的某个宝石开始,顺指针绕一圈,沿途记下经过的宝石,比如项链: 0-1-2-3 ,它的可能的四种表示是0123、1230、2301、3012。
达达现在心急如焚,于是他找到了你,希望你能够编写一个程序,判断两个给定的描述是否代表同一个项链(注意,项链是不会翻转的)。
也就是说给定两个项链的表示,判断他们是否可能是一条项链。
字符串的最小表示,easy…
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
// 字符串的最小表示
string get(string s){
int i = 0, j = 1;
int n = s.length() / 2;
while(i < n && j < n) {
int k = 0;
while(k < n && s[i + k] == s[j + k]) k++;
if(k == n) break;
if(s[i + k] < s[j + k]) j += k + 1;
else i += k + 1;
if(i == j) j++;
}
return s.substr(min(i, j), n);
}
int main(){
string s1, s2;
cin >> s1 >> s2;
s1 += s1;
s2 += s2;
// cout << get(s1) << " " << get(s2) << endl;
if(get(s1) == get(s2)) {
cout << "Yes" << endl;
cout << get(s1) << endl;
}
else cout << "No" << endl;
return 0;
}
奶牛矩阵
每天早上,农夫约翰的奶牛们被挤奶的时候,都会站成一个R行C列的方阵。
现在在每个奶牛的身上标注表示其品种的大写字母,则所有奶牛共同构成了一个R行C列的字符矩阵。
现在给定由所有奶牛构成的矩阵,求它的最小覆盖子矩阵的面积是多少。
如果一个子矩阵无限复制扩张之后得到的矩阵能包含原来的矩阵,则称该子矩阵为覆盖子矩阵。
我们可以先按行暴力计算出每行可行的覆盖长度,再从中取最小,记为width。
然后我们再用kmp匹配的办法计算其next数组,next[n]即是前缀和后缀重合的最大长度。将每行width宽度的字符串视为一个字符,按列的方向求出next数组,我们就能得到列的最小覆盖高度height = n - next[n],如图所示。width和height相乘即是最终结果。
#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
const int M = 80, N = 10010;
int n, m;
bool flag[M];
int ne[N];
char s[N][M];
int main() {
cin >> n >> m;
memset(flag, 1, sizeof flag);
// flag中标志索引宽度是否满足要求,i是每一行,j是每一个宽度,k是j的倍数,u是小于j宽的匹配点
for(int i=1; i<=n; i++) {
scanf("%s", s[i]);
for(int j=1; j<m; j++) {
if(flag[j]) {
for(int k = j; k < m; k += j){
for(int u = 0; u < j && k + u < m; u++) {
if(s[i][u] != s[i][k + u]) {
flag[j] = false;
break;
}
}
if(!flag[j]) break;
}
}
}
}
// 找到最小宽度
int width;
for(int i = 1; i <= m; i++) {
if(flag[i]) {
width = i;
break;
}
}
// 处理每行变成width宽度
for(int i = 1; i <= n; i++) s[i][width] = 0;
// 求next数组
for(int i=2, j=0; i <= n; i++) {
while(j && strcmp(s[i], s[j + 1])) j = ne[j];
if(!strcmp(s[i], s[j + 1])) j++;
ne[i] = j;
}
int height = n - ne[n];
cout << height * width << endl;
return 0;
}
匹配统计
利用在数据结构与算法课上学到的知识,他很容易地求出了“字符串A从任意位置开始的后缀子串”与“字符串B”匹配的长度。
不过阿轩是一个勤学好问的同学,他向你提出了Q个问题:
在每个问题中,他给定你一个整数x,请你告诉他有多少个位置,满足“字符串A从该位置开始的后缀子串”与B匹配的长度恰好为x。
例如:A=aabcde,B=ab,则A有aabcde、abcde、bcde、cde、de、e这6个后缀子串,它们与B=ab的匹配长度分别是1、2、0、0、0、0。
因此A有4个位置与B的匹配长度恰好为0,有1个位置的匹配长度恰好为1,有1个位置的匹配长度恰好为2。
可以利用kmp匹配,先求出p的next数组。我们的目标是求解一个
f
(
x
)
f(x)
f(x)函数,表示匹配数大于等于x的个数,那么对于匹配数为x的个数就是
f
(
x
)
−
f
(
x
+
1
)
f(x)-f(x+1)
f(x)−f(x+1)。那么问题就是如何利用next数组求解
f
(
x
)
f(x)
f(x)。
假设对于匹配长度
i
i
i,已知
n
e
x
t
(
j
)
=
i
next(j)=i
next(j)=i,那么匹配长度为i的数量一定包括匹配长度为j的数量,同理对于任意
n
e
x
t
(
k
)
=
i
next(k)=i
next(k)=i,都有上述结论。因此,我们可以推出如下公式
f
(
n
e
x
t
(
i
)
)
=
f
(
i
)
+
f
(
n
e
x
t
(
i
)
)
f(next(i)) = f(i)+f(next(i))
f(next(i))=f(i)+f(next(i))。而
f
(
x
)
f(x)
f(x)的初始值可以由kmp匹配的过程求解得到。
#include <iostream>
#include <algorithm>
#include <string.h>
#include <unordered_map>
using namespace std;
int n, m, q, x;
int ne[200010], f[200010];
string s, p;
unordered_map<int, int> h;
int main() {
// n, m是s, p的长度
cin >> n >> m >> q;
cin >> s >> p;
p = ' ' + p;
s = ' ' + s;
// 求解p的next数组
for(int i=2, j=0; i <= m; i++) {
while(j && p[i] != p[j + 1]) j = ne[j];
if(p[i] == p[j + 1]) ++j;
ne[i] = j;
}
// 求解s和p的匹配
for(int i=1, j=0; i <=n; i++) {
while(j && s[i] != p[j + 1]) j = ne[j];
if(s[i] == p[j + 1]) ++j;
// 对于每个i,我们要把匹配长度累加
f[j] ++;
}
// 求解f(x)
for(int i=m; i >= 1; i--) {
f[ne[i]] += f[i];
}
while(q--) {
cin >> x;
cout << f[x] - f[x + 1] << endl;
}
return 0;
}
黑盒子
黑盒子代表一个原始的数据库。
它可以用来储存整数数组,并且它拥有一个特殊变量i。
在最开始,黑盒子是空的,并且i=0。
现在对黑盒子进行一系列的操作处理,操作包括以下两种:
1、ADD(x):表示将x加入到黑盒子中。
2、GET:使i增加1,输出黑盒子中第i小的数值(即将所有数按升序排序后的第i个数)。
下面给出一个具体例子:
序号 | 操作 | i | 盒子内数(升序排列后) | 输出的值 |
---|---|---|---|---|
1 | ADD(3) | 0 | 3 | |
2 | GET | 1 | 3 | 3 |
3 | ADD(1) | 1 | 1,3 | |
4 | GET | 2 | 1, 3 | 3 |
5 | ADD(-4) | 2 | -4, 1, 3 | |
6 | ADD(2) | 2 | -4, 1, 2, 3 | |
7 | ADD(8) | 2 | -4, 1, 2, 3, 8 | |
8 | ADD(-1000) | 2 | -1000, -4, 1, 2, 3, 8 | |
9 | GET | 3 | -1000, -4, 1, 2, 3, 8 | 1 |
10 | GET | 4 | -1000, -4, 1, 2, 3, 8 | 2 |
11 | ADD(2) | 4 | -1000, -4, 1, 2, 2, 3, 8 |
分别用大顶堆和小顶堆,控制大顶堆中元素个数,即可实现快速查询。
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 30010;
// a是输入数组,u是get以后数组的个数
int a[N], u[N];
int m, n;
int main() {
cin >> m >> n;
for(int i=0; i<m; i++) cin >> a[i];
for(int i=1; i<=n; i++) cin >> u[i];
// down是大顶堆,up是小顶堆
priority_queue<int> down;
priority_queue<int, vector<int>, greater<int>> up;
// index是a数组的输入顺序
int index = 0;
for(int i=0; i<n; i++) {
for(int j=0; j<u[i+1] - u[i]; j++) {
// 如果大于大顶堆堆顶元素,则压入小顶堆,否则压入大顶堆
if(down.empty() || a[index] > down.top()) up.push(a[index]);
else {
up.push(down.top());
down.pop();
down.push(a[index]);
}
index++;
}
// 弹出小顶堆元素到大顶堆,该元素即使当前查询结果
down.push(up.top());
up.pop();
cout << down.top() << endl;
}
}
生日礼物
翰翰18岁生日的时候,达达给她看了一个神奇的序列 A1,A2,…,AN。
她被允许从中选择不超过 M 个连续的部分作为自己的生日礼物。
翰翰想要知道选择元素之和的最大值。
你能帮助她吗?
我们首先可以礼物值是0的礼物我们可以剔除掉,对最终结果没有影响。
其次,考虑将所有礼物值大于0的正数连续段视为一个整体,小于0的负数连续段视为一个整体。因为我们的选择必定是选取某一整段,不可能出现选一个连续正数的一部分,这样就不如正数全选,也不可能只选取一个连续负数段的一部分,这样不如不选。因此,我们的问题就转化成选哪几段。
类比二叉堆中的数据备份题目。当正数段数小于m的时候,自然可以把所有正数段选取。当正数段数大于m的时候,我们必定要舍弃几段正数段,或者将某些负数段考虑进来,使正数段相连形成连续段。
算法流程先将所有正数和相加,建立一个小根堆,每次选取堆顶元素去除,这样我们选择的连续段数就减少一个。同时,我们可以知道相邻的两段不会同时选取,因为不可能同时去除一段正数,加入一段负数。这样题目就可以完全转化为之前的数据备份。
需要考虑的特殊情况就是,对于边界为负数的情况,我们不会考虑,因为其边界侧已没有正数段,加入该段负数只会减少最终结果。
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
const int N = 100010;
int l[N], r[N], a[N];
bool exist[N];
int m, n, cnt, res, num;
void del(int v) {
r[l[v]] = r[v];
l[r[v]] = l[v];
exist[v] = true;
}
int main() {
// n,m代表礼物个数和连续段长度
cin >> n >> m;
// 去0,合并成一个正负分开的数组,每个数代表一段连续正数或负数的和
for(int i=0; i<n; i++) {
cin >> num;
if(!num) continue;
if(i && a[cnt] * num < 0) a[++cnt] += num;
else a[cnt] += num;
}
// 建立一个堆
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> q;
// 更新n为数组a的长度,cnt是正数段个数
n = cnt;
cnt = 0;
// 形成双链表,res表示正数总和
for(int i=1; i<=n; i++) {
if(a[i] > 0) {
res += a[i];
cnt++;
}
q.push({abs(a[i]), i});
l[i] = i-1;
r[i] = i+1;
}
// 如果当前连续段个数多于m,就删减
while(cnt > m) {
auto top = q.top();
q.pop();
int pos = top.second;
// 如果当前堆顶是已经删除的,或者是位于首尾的负数,那么我们跳过
if((a[pos] < 0 && (!l[pos] || r[pos] == n + 1)) || exist[pos]) continue;
res -= top.first;
cnt --;
int left = l[pos];
int right = r[pos];
a[pos] += a[left] + a[right];
q.push({abs(a[pos]), pos});
del(left);
del(right);
}
cout << res << endl;
return 0;
}