2021牛客寒假算法基础集训营2
A-牛牛与牛妹的RMQ
题意:
思路:
这种题目就是多种数据结构的组合题,从一种意义上说也就是模板题,要好好掌握,熟练写出。
枚举所有端点获得最值的贡献显然是不可能的,那么我们就可以考虑算每个最值的贡献会发生在哪些区间。
最值是具有可合并性的,就是当前区间的最值一定是包含的所有可枚举端点组成的区间之中活动的,所以我们只需要对于每次查询的k,把给定的数组排一下序,那么所有在选取端点的过程中可能出现的最值,就是每两个相邻端点构成的区间最值再加上可选取端点处的端点值。
这样我们一共就可以获得2k-1个可能的最值(不去重的情况下)。
对于每个最大值,我们还要获得选哪些端点时,他是最大的,由于我们只考虑概率,所有的就是需要端点的数量。
用单调栈求出,每个最值作为最大值的区间范围,然后对于每次询问的k,取用树状数组维护,在它的区间范围内,有多少个可选的下标。
假如某个数当前位置是pos,它作为最大值出现的区间是[L,R],那么假设区间[L,pos]内的可选的下标数量为x,区间[pos,R]内可选的下表数量为y,如果pos这个位置是某个可选的下标,那么这个最值贡献的答案就是 xy2 - 1,减去[pos,pos]这个重复的情况,否则贡献就是xy2。
最后注意一下,每次初始化时的范围大小,不要超时。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 7;
const int M = 2e5 + 7;
#define pb emplace_back
#define pil pair<int,ll>
// #define int long long
const int INF = 0x3f3f3f3f;
long long gcd(long long x, long long y)
{
if (y == 0) return x;
return gcd(y, x %y);
}
int n,a[N],p[N];
/***************1.********************/
int mx[N<<2];
void pushup(int rt) {
mx[rt] = max(mx[rt<<1],mx[rt<<1|1]);
}
void build(int rt,int l,int r) {
if(l == r){
mx[rt] = a[l];return ;
}
int mid = (l + r) >> 1;
build(rt<<1,l,mid);build(rt<<1|1,mid+1,r);
pushup(rt);
}
// void modify(int rt,int l,int r,int p,int v)
inline int query(int rt,int l,int r,int L,int R) {
if(l >= L && r <= R) return mx[rt];
int mid = (l + r) >> 1,ans = 0;
if(L <= mid) ans = max(ans,query(rt<<1,l,mid,L,R));
if(R > mid) ans = max(ans,query(rt<<1|1,mid+1,r,L,R));
return ans;
}
/***************1.********************/
/***************2.********************/
int c[N<<2];
inline int lowbit(int x){ return x & (-x); }
inline void add(int pos,int fg) {
c[pos] += fg;
while(pos <= n) {
pos += lowbit(pos);
c[pos] += fg;
}
}
inline int qsum(int pos) {
int res = 0;
while(pos) { res += c[pos];pos -= lowbit(pos); }
return res;
}
/***************2.********************/
int L[N],R[N],sta[N],pp[N],vis[N],tmp_max[M];
pil ans[M];
int main() {
scanf("%d",&n);
for(int i = 1;i <= n;i ++) {
scanf("%d",&a[i]);
pp[a[i]] = i;
}
build(1,1,n);
int top = 0;//单调栈求出 当前的值 作为最大值的左右区间
for(int i = 1;i <= n;i ++) {
while(top && a[i] >= a[sta[top]]) top--;
if(!top) L[a[i]] = 1;
else L[a[i]] = sta[top] + 1;
sta[++top] = i;
}
top = 0;
for(int i = n;i >= 1;i --) {
while(top && a[i] >= a[sta[top]]) top--;
if(!top) R[a[i]] = n;
else R[a[i]] = sta[top] - 1;
sta[++top] = i;
}
int m,k;scanf("%d",&m);
while(m--) {
scanf("%d",&k);
// int mxp = 0;
for(int i = 1;i <= k;i ++) scanf("%d",&p[i]) , vis[a[p[i]]] = 1;
sort(p + 1,p + 1 + k);
//找出来对应区间
int siz = 0;
for(int i = 1;i <= k;i ++) {
add(p[i],1);
// ss.insert(a[p[i]]);
tmp_max[++siz] = a[p[i]];
}
// cout<<"------\n";
for(int i = 1;i < k;i ++) {
int tmp = query(1,1,n,p[i],p[i+1]);
if(!vis[tmp]) tmp_max[++siz] = tmp;
}
// cout<<"------\n";
sort(tmp_max + 1,tmp_max + 1 + siz);
int cnt = 0;
for(int i = 1;i <= siz;i ++) {
int val = tmp_max[i];
int l = L[val],r = R[val];
ll lsum,rsum;
if(l == 1) lsum = qsum(pp[val]);
else lsum = qsum(pp[val]) - qsum(l - 1);
if(pp[val] == 1) rsum = qsum(r);
else rsum = qsum(r) - qsum(pp[val] - 1);
if(vis[val]) {
ans[++cnt].first = val;
ans[cnt].second = lsum * rsum * 2 - 1;
}
else {
ans[++cnt].first = val;
ans[cnt].second = lsum * rsum * 2;
}
}
for(int i = 1;i <= cnt;i ++) {
ll tmp = gcd(1ll * k * k,ans[i].second);
printf("%d %lld/%lld\n",ans[i].first,(ans[i].second/tmp),(1ll*k*k/tmp));
}
for(int i = 1;i <= k;i ++) {//这个地方千万别写n TLE吐了.....
add(p[i],-1);
vis[a[p[i]]] = 0;
}
}
return 0;
}
C- 牛牛与字符串border
题意:
思路:
构造题多画画,多找找规律。
需要构造一个字符串使它满足长度为l,2l,3l…kl的border都存在,那么上来比较明显的就是,找到一个尽可能长的循环节,是整个字符串由此表示,由样例我们可以猜测,这个gcd(n,l)是不是就是我们要构造的长度,但是这种情况下,假如
l
∗
2
>
n
l* 2 > n
l∗2>n,也就是border最长就为l,那么按照之前的构造我们要把整个串构造为一个字符,但是因为最长的border只有l,我们如果用n-l构造,俺那么2倍的长度也不会超,这样长度为
2
∗
(
n
−
l
)
2 * (n-l)
2∗(n−l)的数组,就有n-l的部分是相同的了,再循环进行下去吗,他们多出来的部分也一定能相等的,实在不理解找个例子看看,很直观。
最后得出的字符串的border也是正好就是l,并且总是等于l。
由 此 , l ∗ 2 < = n 时 , 循 环 节 就 是 g c d ( n , l ) , l ∗ 2 > n 时 , 循 环 节 就 是 n − k 由此,l * 2 <= n时,循环节就是gcd(n,l), l * 2 > n时,循环节就是n-k 由此,l∗2<=n时,循环节就是gcd(n,l),l∗2>n时,循环节就是n−k
用一个桶维护,循环节的对应位置的字符应该是哪个,取出现的字符众数即,最后循环拼接一下。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 7;
const int INF = 0x3f3f3f3f;
//字符串题就多画图看看性质规律
//如果l * 2 <= n,循环节就是gcd(l,n),如果是一的话,就去找出现最多的那个字符
//如果l * 2 > n,那么就取n - l,根据题意定义的border可以证明这是最长的,再长的话,就必须和前面重复相等了
int gcd(int a,int b) {
int r;
while(a % b != 0) { r = a % b; a = b; b = r; }
return b;
}
int cal(int x,int k) { return x % k == 0 ? k : x % k; }
int bk[MAXN][26],mx[MAXN];
char ans[MAXN];
char s[MAXN];
int main() {
int T;scanf("%d",&T);
while(T--) {
int n,l;scanf("%d%d",&n,&l);
scanf("%s",s + 1);
int ti;
if(l * 2 <= n) ti = gcd(n,l);
else ti = n - l;
for(int i = 1;i <= n;i ++) {
int p = cal(i,ti);
bk[p][s[i]-'a']++;
if(mx[p] < bk[p][s[i]-'a']) {
mx[p] = bk[p][s[i]-'a'];
ans[p] = s[i];
}
}
for(int i = 1;i <= n;i ++) {
int p = cal(i,ti);
printf("%c",ans[p]);
}
puts("");
for(int i = 1;i <= ti;i ++) {
memset(bk[i],0,sizeof(bk[i]));
mx[i] = 0;
}
}
return 0;
}
D-牛牛与整除分块
题意:
思路:
不要害怕,多思考,找方法。
和整除分块没啥关系其实,就是用到了,分块的一个思想,前 n \sqrt{n} n个数,一定是一个数对应着一个整除的值,但后面的数就有可能对应多个值了,但是这道题只让我们看是去重后的第几大,那么观察一下,可能发现从 n + 1 \sqrt{n} + 1 n+1开始,后面的值一定会连续出现,并且不会空缺,也就是如果当前为 x x x,那么后面就一定是 x − 1 , x − 2 , . . . . , 2 , 1 x-1,x-2,....,2,1 x−1,x−2,....,2,1,所以就以 n \sqrt{n} n为间隔分开即可。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
int T;scanf("%d",&T);
while(T--) {
int n,x;scanf("%d%d",&n,&x);
int t = sqrt(n) + 1;
if(x <= t) {
printf("%d\n",x);
}
else {
int ans = t + n/t - n/x;
printf("%d\n",ans);
}
}
return 0;
}
E-牛牛与跷跷板
题意:
其实看到连续木板之间可以跳跃并且求得是最小跳跃次数,就反应除了应该是bfs或者是最短路,然后问题就变成了,怎么把图建好,建好图之后直接bfs就可。
先考虑上下层之间。
肯定不能
n
2
n^2
n2的建图,考虑怎么优化一下,如果找到了最右侧的能和当前模板满足跳跃条件的模板,那么对于下一块木板,是否可以舍弃一些木板直接不去计算也知道他们是不可以跳跃的,答案是可以的。
因为题目中说到不会有重叠的木板,那么对于一个木板,假设它右侧第一个和它无法完成跳跃的木板是k,那么只有k-1及其以后的木板是可能和下一块木板能完成跳跃的,因为如果k-2可的话,那么最右侧的板就应该是k-1,这是与前面矛盾的。
到此,下一层的木板和当前层的木板其实是符合单调性的,因此我们可以尺取出,每一层每个位置能够连接的点,从而在
O
(
n
)
O(n)
O(n)的时间内建图。
再考虑左右层
左右层更简单,只需要排序后,看每相邻的两块之间是否左右连接即可。
建好图后,bfs求答案,注意细节。
多考虑数据结构对于时间复杂度的优化。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 2e5 + 7;
#define pb emplace_back
#define MP make_pair
#define pii pair<int,int>
const int INF = 0x3f3f3f3f;
const int eps = 1e-8;
//其实就是预处理一下 跷跷板之间的连接关系就可了
struct node{
int id,l,r,step;
}bl[MAXN];
vector<node>G[MAXN];//存一下当前纵坐标下的板子有哪些?
bool cmp(node a,node b) {
if(a.l != b.l) return a.l < b.l;
else return a.r < b.r;
}
int head[MAXN],cnt,vis[MAXN];
struct Edge{
int next,to;
}edge[MAXN<<2];
void add(int u,int v){
edge[++cnt].to = v; edge[cnt].next = head[u]; head[u] = cnt;
edge[++cnt].to = u; edge[cnt].next = head[v]; head[v] = cnt;
}
int main() {
int n;
scanf("%d",&n);
int y,a,b,mxy = 0;
for(int i = 1;i <= n;i ++) {
scanf("%d%d%d",&y,&a,&b);
// Node t(i, a, b);
node t;t.id = i,t.l = a,t.r = b;
G[y].pb(t);
mxy = max(y,mxy);
}
for(int i = 0;i <= mxy;i ++) {
sort(G[i].begin(),G[i].end(),cmp);
}
//因为总共就是1e5块板 但是没有重合的板 尺取一下 应该是O(n)的
for(int i = 0;i <= mxy;i ++) {
int k = 0,siz = G[i].size(),siz1 = G[i+1].size();
for(int j = 0;j < siz;j ++) {
if(j){
if(G[i][j-1].r == G[i][j].l)
add(G[i][j-1].id,G[i][j].id);
}
if(i < mxy) {
while(k < siz1 && G[i+1][k].r <= G[i][j].l) ++k;
while(k < siz1 && G[i+1][k].l < G[i][j].r) {
add(G[i+1][k].id,G[i][j].id);
++k;
}
--k;//因为只保证了当前的左端点可行性的极限 但是可能前面的板的右端有点仍能连到下个板
//而板之间不重合 保证了只减一就能保证答案的正确性
}
}
}
int ans;
queue<pii>que;
pii s;s .first = 1,s.second = 0;
que.push(s);
vis[1] = 1;
while(!que.empty()) {
pii pre = que.front();
que.pop();
if(pre.first == n) {
ans = pre.second;
break;
}
for(int i = head[pre.first];i;i = edge[i].next) {
int v = edge[i].to;
if(!vis[v]) {
vis[v] = 1;//不能放在外面 要不谈 一层之间的会相互影响
pii now;now.first = v,now.second = pre.second + 1;
que.push(now);
}
}
}
printf("%d\n",ans);
return 0;
}
G-牛牛与比赛颁奖
题意:
思路:
其实很明显看出来一定要差分操作,但是
n
≤
1
e
9
n \leq 1e9
n≤1e9这个范围太大了,但是看
m
≤
1
e
5
m \leq 1e5
m≤1e5就知道了需要离散化操作一下,因为通过的题目队伍一定是连续的,我们就不关心没出现的队伍了,只需要知道他在那两个通过题目数之间即可。
这样就可以,枚举全场可能通过的题目数,从而获得每个题数下有多少只队伍通过,知道通过了队伍数,就可以按照过题数模拟出奖牌线了,最后只需要枚举题目数,统计通过的队伍总数即可。
总而言之,就是比较吃细节的模拟,慢点细心点写就没问题。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 1e6 + 7;
struct node {
int l,r;
}op[MAXN];
int b[MAXN],d[MAXN],tot[MAXN],n,m;
int cal(int x) {//求奖牌线的函数
int sum = 0;
for(int i = m;i >= 0;i --) {
if(sum + tot[i] >= x) return i;
else sum += tot[i];
}
return 0;
}
int main() {
scanf("%d%d",&n,&m);
int cnt = 0;
for(int i = 1;i <= m;i ++) {
scanf("%d%d",&op[i].l,&op[i].r);
b[++cnt] = op[i].l;
b[++cnt] = op[i].r;
b[++cnt] = op[i].r + 1;
}
b[++cnt] = 1,b[++cnt] = n + 1;//做的是前缀和统计 所以要插入这两个特殊位置的值 才能使得操作统一化
sort(b + 1,b + 1 + cnt);
int num = unique(b + 1,b + 1 + cnt) - b - 1;
for(int i = 1;i <= m;i ++) {
int l = lower_bound(b + 1,b + 1 + num,op[i].l) - b;
int r = lower_bound(b + 1,b + 1 + num,op[i].r) - b;
// cout<<l<<' '<<r<<'\n';
d[l]++;
d[r + 1]--;
}
for(int i = 1;i <= num;i ++) {
d[i] += d[i - 1];//前缀和统计当前位置 通过的题目数是多少
tot[d[i]] += b[i + 1] - b[i];// tot数组记录通过当前题目数的队伍数是多少
// cout<<i<<": "<<d[i]<<' '<<tot[d[i]]<<'\n';
}
//算 奖牌线
int gold = max(1,cal((n + 9) / 10));// n/i向上取整 = (n + i - 1) / i 向下取整
int sliver = max(1,cal((n + 3) / 4));
int bronze = max(1,cal((n + 1) / 2));
// cout<<gold<<' '<<sliver<<' '<<bronze<<'\n';
int ans1 = 0,ans2 = 0,ans3 = 0;
for(int i = 0;i <= m;i ++) {
// cout<<tot[i]<<"***\n";
if(i >= gold) ans1 += tot[i];
else if(i >= sliver) ans2 += tot[i];
else if(i >= bronze) ans3 += tot[i];
}
printf("%d %d %d\n",ans1,ans2,ans3);
return 0;
}