文章目录
前言
这一个专题就是把之前训练里面,没有涉及到的蓝桥杯题目咱们讲一讲!
一、修改数组(多种写法,超nice)
任意门
感觉这道题目有一点点哈希表那味。
方法一:平衡树set
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
方法二:并查集
O
(
n
+
m
)
O(n+m)
O(n+m)
写法一:
#include <cstdio>
#include <algorithm>
#include<map>
using namespace std;
const int N=1100010;
int p[N];
int find(int x)
{
if(p[x]!=x)p[x]=find(p[x]);
return p[x];
}
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<N;i++)p[i]=i;
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
x=find(x);
printf("%d ",x);
p[x]=x+1;
}
return 0;
}
写法二:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std ;
const int N = 1000010 ;
int st[N] ;
//每次+1会超时,可以采用 “跳”
int main(){
int n ;
cin >> n ;
for(int i=0;i<n;i++){
int a ;
cin >> a ;
while(st[a]){
a = st[a] ++ ;
}
st[a] = a;
cout << a << ' ' ;
}
return 0 ;
}
写法三:平衡树写法
c++里面的话,平衡树就是set和map
将每次询问的值加入区间中并进行不断的合并,询问。因为每次询问的值可能在已知区间内,所有要注意upper_bound求的时候要加入{x,inf}。
二分写法:
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<set>
#include<climits>
using namespace std;
int n;
typedef pair<int, int> PII;
set<PII> sgs;
int main() {
int x;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &x);
if (i == 0) {
sgs.insert({ x,x });
printf("%d ", x);
continue;
}
//在集合中去找一个区间包含x的,本来我们只需要找一个数x
//但是由于set里面存的区间,一个数和一个区间肯定无法直接比较大小
//所以我们要把这个数x变成一个区间,即<x,INT_MIN>,就可以用lower_bound查找了
//找到的是第一个大于或等于这个pair的pair(pair先比较第一个关键字的大小。
//如果相等,再比较第二个关键字的大小)。
//二分查找包含x的区间
auto it = sgs.lower_bound(PII(x, INT_MIN));
//如果包含 即[*,?] (?>=x)
if (it != sgs.end() && it->second <= x) {
//printf("包含\n");
//修改x为这个区间的右端点+1,即让x脱离这个区间。
x = it->first + 1;
//合并这两个区间,且必然能合并
int l = it->second;//备份区间左值
sgs.erase(it);//删除原来的区间
sgs.insert(PII(x, l));//插入新的区间
//我们去找一个区间的右端点大于等于x+1
//即[3,x]->[?,?] (?>=x+1)
it = sgs.lower_bound({ x,l });
auto back = sgs.lower_bound(PII(x+1, INT_MIN));
if (back != sgs.end() && back->second == x + 1) {
//如果找到了这样的一个pair(右端点大于x,左端点等于x+1)
//那么这两个区间又可以合并了
int r = back->first;
int l = it->second;
sgs.erase(it);
sgs.erase(back);
sgs.insert(PII(r, l));
}
}
else {
//没有一个区间包含x,那么x就不需要修改
//先插入到set里面
sgs.insert(PII(x, x));
//检查是否可以合并 [?,x-1],[x,x],[x+1,?]
//找右端点大于等于x-1的
auto it = sgs.lower_bound(PII(x, x));//先定位
auto pre = it;
if (pre != sgs.begin()) {
pre--;
if (pre->first + 1 == it->second) {
//合并
int l = pre->second;
int r = it->first;
sgs.erase(pre);
sgs.erase(it);
sgs.insert({ r,l });
}
}
//定位[?,x]
it = sgs.lower_bound(PII(x, INT_MIN));
auto back = sgs.lower_bound({ x + 1,INT_MIN });
if (back != sgs.end() && back->second == it->first + 1) {
//合并
int l = it->second;
int r = back->first;
sgs.erase(it);
sgs.erase(back);
sgs.insert(PII(r, l));
}
}
printf("%d ", x);
}
printf("\n");
return 0;
}
写法二:
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int>PII;
const int N=100010;
int main()
{
int n;
cin>>n;
set<PII>segs;
while(n--)
{
int x;
cin>>x;
auto it=segs.lower_bound({x,INT_MIN});//返回第一个大于等于x的数,x为右边的端点
if(it!=segs.end()&&it->second<=x)x=it->first+1;//如果是落在已经存在的区间里面
cout<<x<<" ";
segs.insert({x,x});
if(it!=segs.begin())it--;//找到it的前驱节点
for(int i=0;i<=2&&it!=segs.end();i++)//合并他后面的两次,避免合并后的又要再合并一次
{
auto j=it;
j++;
if(j!=segs.end()&&it->first+1==j->second)//first是右端点,second是左端点
{
int l=it->second,r=j->first;
segs.insert({r,l});
segs.erase(it);
segs.erase(j);
it=segs.find({r,l});
}
else it=j;
}
}
cout<<endl;
return 0;
}
这个方法只能过一部分数据,当数据过大的时候无法过,甚至在蓝桥杯官网的编译是过不了的!!因为不认auto。所以不能使用to_string、stoi、stol、auto、unordered_map、unordered_set这些好用的函数啦~
但是有网友说现在比赛认了嘿嘿。
写法四:略带暴力法
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,a[100001];
bool book[1000001]={0};//标记数组
cin>>n;
for(int i=0;i<n;i++){//输入
cin>>a[i];
}
book[a[0]]=1;
for(int i=1;i<n;i++){
if(book[a[i]]==0){//没有出现过
book[a[i]]=1;//标记位置为1
continue;
}
else{//出现过
while(book[a[i]]!=0){//一直加一直到没出现过
a[i]+=1;
}
book[a[i]]=1;//将符合条件的数加一
}
}
for(int i=0;i<n;i++){//输出
cout<<a[i]<<" ";
}
return 0;
}
写法五:线段树写法
#include<iostream>
using namespace std;
const int MAX_N=1e5;
const int SIZE=1e6+1e5+5;
int tree[SIZE<<2];//维护最大值,用过的点换成0
int tsize=1;
inline int ls(int p){return p<<1|1;}
inline int rs(int p){return (p<<1)+2;}
void init(int n)//n为输入数据最大值加1e5+5(保险)
{
tsize=1;
while(tsize<n)tsize<<=1;
int last_left=tsize-1;
for(int i=last_left;i<last_left+tsize;i++)tree[i]=i-last_left;
while(last_left)
{
for(int i=(last_left-1)>>1;i<last_left;i++)tree[i]=max(tree[ls(i)],tree[rs(i)]);
last_left=(last_left-1)>>1;
}
}
void pull_up(int p)
{
tree[p]=max(tree[ls(p)],tree[rs(p)]);
}
int query(int x,int p=0,int l=0,int r=tsize)
{
if(l+1==r)
{
tree[p]=0;
return l;
}
int ans;
if(tree[ls(p)]>=x)ans=query(x,ls(p),l,(l+r)>>1);
else ans=query(x,rs(p),(l+r)>>1,r);
pull_up(p);
return ans;
}
int main()
{
int n;
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int m=0;
cin>>n;
int x;
init(1e6+1e5+5);
for(int i=0;i<n;i++)
{
cin>>x;
cout<<query(x)<<" ";
}
return 0;
}
二、倍数问题(背包问题)
任意门
这一道题就是通过选取数字得到k的最大整数倍。
这样子的组合问题求最优解就是典型的背包问题—组合问题求最优解。
左右两边分别求最大值,然后取一个max就可以了。
#include<bits/stdc++.h>
using namespace std;
//我们这里用vector来存余数前三大的数
const int N=1010;
int n,m;
vector<int>a[N];
int f[4][N];
int main()
{
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++)
{
int x;
scanf("%d",&x);
a[x%m].push_back(x);
}
memset(f,-0x3f,sizeof f);
f[0][0]=0;
for(int i=0;i<m;i++)
{
sort(a[i].begin(),a[i].end());
reverse(a[i].begin(),a[i].end());
for(int u=0;u<3&&u<a[i].size();u++)
{
int x=a[i][u]; //我们只用去看那个余数的最大的三个数
for(int j=3;j>=1;j--)
for(int k=0;k<m;k++)
f[j][k]=max(f[j][k],f[j-1][(k-x%m+m)%m]+x);
}
}
printf("%d\n",f[3][0]);
return 0;
}
三、斐波那契(数论、这一题自知能力不行,我跳过了)
任意门
将问题转化一下,就会简单很多。但是想到真的是难!!!
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
LL p;
LL qmul(LL a, LL b)
{
LL res = 0;
while (b)
{
if (b & 1) res = (res + a) % p;
a = (a + a) % p;
b >>= 1;
}
return res;
}
void mul(LL c[][2], LL a[][2], LL b[][2]) // c = a * b
{
static LL t[2][2];
memset(t, 0, sizeof t);
for (int i = 0; i < 2; i ++ )
for (int j = 0; j < 2; j ++ )
for (int k = 0; k < 2; k ++ )
t[i][j] = (t[i][j] + qmul(a[i][k], b[k][j])) % p;
memcpy(c, t, sizeof t);
}
LL F(LL n)
{
if (!n) return 0;
LL f[2][2] = {1, 1};
LL a[2][2] = {
{0, 1},
{1, 1},
};
for (LL k = n - 1; k; k >>= 1)
{
if (k & 1) mul(f, f, a); // f = f * a
mul(a, a, a); // a = a * a
}
return f[0][0];
}
LL H(LL m, LL k) // (F(m - 1) * F(k) - 1) mod F(m)
{
if (k % 2) return F(m - k) - 1;
else
{
if (k == 0 || m == 2 && m - k == 1) return F(m) - 1;
else return F(m) - F(m - k) - 1;
}
}
LL G(LL n, LL m) // (F(n) - 1) mod F(m)
{
if (m % 2 == 0) // m是偶数
{
if (n / m % 2 == 0) // n / m 是偶数
{
//cout << n << ' ' << m << ' ' << F(n % m) << endl;
if (n % m == 0) return F(m) - 1;
else return F(n % m) - 1;
}
else // n / m 是奇数
{
return H(m, n % m);
}
}
else // m 是奇数
{
if (n / m % 2 == 0 && n / 2 / m % 2 == 0) // n / m 是偶数,n / (2m) 是偶数
{
if (n % m == 0) return F(m) - 1;
else return F(n % m) - 1;
}
else if (n / m % 2 == 0 && n / 2 / m % 2) // n / m 是偶数,n / (2m) 是奇数
{
if (m == 2 && n % m == 1) return F(m) - 1;
else return F(m) - F(n % m) - 1;
}
else if (n / m % 2 && n / 2 / m % 2 == 0) // n / m 是奇数,n / (2m) 是偶数
{
return H(m, n % m);
}
else // n / m 是奇数,n / (2m) 是奇数
{
if (n % m % 2)
{
if (m == 2 && m - n % m == 1) return F(m) - 1;
else return F(m) - F(m - n % m) - 1;
}
else
{
return F(m - n % m) - 1;
}
}
}
}
int main()
{
LL n, m;
while (cin >> n >> m >> p) cout << (G(n + 2, m) % p + p) % p << endl;
return 0;
}
四、距离(离线Tarjan求LCA算法)
任意门
只要是在一个树上面的话,那么他的两个点之间的距离固定的。
x、y两点的最近共公祖先。
距离为:d(x)+d(y)-2d§
d§就是x、y的最近公共祖先
tarjan算法
大佬!!!
在线做法:边读边做
离线做法:先读完,再全部处理,最后全部输出。
Tarjon本质就是对向上标记法的一个优化,任取一个节点当成根节点进行dfs优先遍历,把所有节点分成三部分
1)已经遍历并且回溯的标记成2
2)正在遍历的没有回溯的标记成1
3)未遍历的标记成0
注意:顺序一定不能乱。
1)当这个点遍历子节点后的时候把子节点的祖宗更新成当前点
2)只有当前点回溯的时候才可以用这个点来计算所有之前为2的点,因为如果当前点为a,而b是a这条路径的上的点,并且b在a的下面,那么因为b先回溯,所以要等b回溯了之后才能正确的判断a.
3)加入询问的时候要加入两次
num[a].push_back = {b, i}, num[b].push_back(a, i};
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
const int N = 10010, M = N * 2;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
int p[N];
int res[M * 2];//存储每个询问的结果
int st[N];
vector<PII> query[N]; // first存查询的另外一个点,second存查询编号
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}//e代表连接的点,w代表这个点的权值,ne代表下个点的下表,h[a]代表这个点的序号,我们用这个序号来标记
void dfs(int u, int fa)
{
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == fa) continue;
dist[j] = dist[u] + w[i];
dfs(j, u);
}
}
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
void tarjan(int u)
{
st[u] = 1;
for (int i = h[u]; ~i; i = ne[i])//开始遍历这这个点所连接的点
{
int j = e[i];
if (!st[j])
{
tarjan(j);
p[j] = u;
}
}
for (auto item : query[u])
{
int y = item.first, id = item.second;
if (st[y] == 2)//y遍历过
{
int anc = find(y);//这是他们的最近公共祖先
res[id] = dist[u] + dist[y] - dist[anc] * 2;
}
}
st[u] = 2;//代表着这个点已经被遍历过了
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 0; i < n - 1; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);//依次读入n-1条边
add(a, b, c), add(b, a, c);
}
for (int i = 0; i < m; i ++ )
{
int a, b;
scanf("%d%d", &a, &b);//读入询问的节点
if (a != b)
{
query[a].push_back({b, i});
query[b].push_back({a, i});
}
}
for (int i = 1; i <= n; i ++ ) p[i] = i;
dfs(1, -1);//处理dist数组
tarjan(1);
for (int i = 0; i < m; i ++ ) printf("%d\n", res[i]);
return 0;
}
五、剪格子
任意门
不能有三种联通的部分。
DFS只能一笔画。而不能我们现在所需要的多笔画。
由于接下来的题目过难,待补