文章目录
- The Second Week
- 一、前言
- 二、算法
- 1.二分图匹配/匈牙利算法
- <1>([ZJOI2007]矩阵游戏)
- <2>(有大家喜欢的零食吗)
- 2.优先队列
- <1>(Running Median)
- <2>(旅途的终点)
- 3. 并查集
- <1>(食物链)
- 4. 搜索与搜索剪枝(DFS)
- <1>(模拟战役)
- <2>(Linear Probing)
- 5.搜索与搜索剪枝(BFS)
- <1>(Jelly)
- 三、总结
The Second Week
耐心和恒心总会得到报酬的。 ————爱因斯坦
一、前言
周二round4,打得不尴不尬吧,四题过不了,三题不够快,继续加油。
周三下午一场牛客,一开始a题一直过不了,但是好多人都过了,僵持了好久换题了,浪费了不少时间,结果回头来看又做出来了。总体感觉还可以吧,能做的都做出来了,做的也还算快,问题是后面一个半小时一题也没ac,后来补题疯狂学算法。
周五round5,打得挺糟糕的,思维不够严谨,有的点考虑不到。分辨不出题型,判断失误导致没有做出该对的题。
周六友谊赛,ac三题,用时罚时都比较长,还间接导致第四题来不及做,脑子🧠锈住了吧你。
二、算法
1.二分图匹配/匈牙利算法
二分图:
有n个点,m条线,每条线连接任意俩个点,如果可以把俩个点分别放进俩个部分,单个部分的点间没有连线,所有线段都任意连接一部分中的一个点,以及另一个部分的任意一个点,就叫作二分图。
二分图基本样式
二分图的最大匹配:
任意一个点最多只能连一条线就是一个匹配,最大的边数就是最大匹配。
蓝色部分与红色部分都是一个最大匹配
黄色部分是一个匹配,但并非最大,紫色部分不是一个匹配
增广路径:
对于一个匹配a,如果有一条交错出现在a的路径,且前后俩个端点不属于a,就把这条路径叫作增广路径。
若红色为匹配a,那么红蓝线就是这个匹配的增广路径。
匈牙利算法:
其实我不知道我能不能想明白…
1.从下部的一个点开始遍历,对于红线下点,找到一条未被匹配过的线段,双方匹配。得到一个最大匹配red。
2.下部第二个点,蓝线下点,进行匹配,也想跟红线上点匹配,但这是不行的,因为每个点只能匹配一个点,它就想让红线换个点。
3.好嘛红线下点就去找了一下,找到了另一条蓝线,于是它俩就匹配了,与此同时此时的所有线加上一条不存在的红线,就是之前匹配red的增广路径blue,且图中连上的线就是red在blue里的相反,必然会比red大。
4.以此类推
<1>([ZJOI2007]矩阵游戏)
题解:
t组数据,每组给出一个n*n的01矩阵,0表示白色,1表示黑色。要求通过行交换与列交换实现对角线的格子均为黑色。
不难看出行列交换难以改变黑色格子的相对位置,所以每一行每一列都必须有至少一个黑色格子。可以将行列看作是俩个集合,一一对应,如果最大路径是n则成立。
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int t,n;
int vis[210],h[210];
int a[210][210];
int li[210];
bool dfs(int x) {
for (int i = 1; i <= n; i++) {
if(!vis[i] && a[x][i]) {
vis[i] = 1;
//用来标记当前这一行,有多少个已经被标记的列
//防止它在找别人时又找了回来,导致俩个行跟同一列的情况
if(!li[i] || dfs(li[i])) {
//如果这一列还没被标记,直接用它
//如果已经被标记,让它去找别人
li[i] = x;
//记录这一列所对应的行
return true;
}
}
}
return false;
}
signed main() {
cin >> t;
while (t--) {
cin >> n;
memset(li,0,sizeof li);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> a[i][j];
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
//对于每一行,寻找自己单独对应的列
memset(vis,0,sizeof vis);
if(dfs(i))ans++;
}
if(ans == n) cout << "Yes" << endl;
else cout << "No" << endl;
}
return 0;
}
<2>(有大家喜欢的零食吗)
题解:
有n个小朋友和n个大礼包,保证任意一个小朋友至少会喜欢一个,求最少还需要购买多少份大礼包来保证所有小朋友都吃到自己喜欢的礼包。
二分图最大匹配板子题。
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n;
int vis[510];
int a[510][510];
int lk[510];
bool dfs(int x) {
for (int i = 1; i <= n; i++) {
if(!vis[i] && a[x][i]){
vis[i] = 1;
if(!lk[i] || dfs(lk[i])) {
lk[i] = x;
return true;
}
}
}
return false;
}
signed main() {
cin >> n;
memset(a,0,sizeof a);
memset(lk,0,sizeof lk);
for (int i = 1; i <= n; i++) {
int k;
cin >> k;
for (int j = 1; j <= k; j++) {
int t;
cin >> t;
a[i][t] = 1;
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
memset(vis,0,sizeof vis);
if(dfs(i))ans++;
}
if(ans == n)cout << "Yes" << endl;
else {
cout << "No" << endl;
cout << n-ans << endl;
}
return 0;
}
2.优先队列
//升序队列,小根堆
priority_queue <int, vector<int>, greater<int>> a;
//降序队列,大根堆,默认的也是这个
priority_queue <int, vector<int>, less<int>>b;
//也可以对string排序,一些基础数据操作跟queue类似
<1>(Running Median)
这题如果能想到用优先队列,题目就很简单了。
题解:
给出t组数据,每组数据编号p,输出数据编号和中位数数量,每当输入奇数索引的数字,都要输出当前数组所对应的中位数。
用俩个优先队列储存数字,然后每次把多出来那一个输出即可,具体看注释。注意,题目对于输出格式有要求。
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
//这三行不加会超时
int t;
cin >> t;
for (int i = 1; i <= t ; i++) {
int p,m;
cin >> p >> m;
cout << p << ' ' << (m+1)/2 << endl;
priority_queue<int,vector<int>,greater<int>>a;
//小根堆,后半部分数组
priority_queue<int>b;
//大根堆,前半部分数组
int ls;
cin >> ls;
cout << ls << " ";
b.push(ls);
for (int j = 2; j <= m; j++) {
cin >> ls;
if(b.top() < ls)a.push(ls);
//大于前半部分的数组
else b.push(ls);
while (a.size() > b.size() + 1) {
b.push(a.top());
a.pop();
}//最多只能多一个,不然就得往另一个队列还回去
while (b.size() > a.size() + 1) {
a.push(b.top());
b.pop();
}
if(a.size() > b.size()) cout << a.top() << ' ';
else if(b.size() > a.size()) cout << b.top() << ' ';
if(j % 20 == 0 && m > j)cout << endl;
}//输出要求
cout << endl;
}
return 0;
}
<2>(旅途的终点)
把这题放上来还有个原因是,我的解法好像跟正解有出入,不过我自认为我的解法还挺简便的。
题解:
有n个国家,必须按照从1到n的顺序畅游这些国家,没经过一次需要耗费ai点生命力,生命力释放完毕则回国,有k次释放神力的机会,不用耗费生命力,求最多可以畅游多少个国家。
稍微讲一下我一开始的错误思路,找到最大的k个,每遇到这些国家时用神力,生命力开始不够的时候也用神力,用完为止。可以想象如果第一个数很大,但不是第k大,耗费大量生命力,后面一连串小国家则无法更多的访问。
我的正确思路应该是对于每插入一个,进行计算需要用几次神力,生命力不够的时候当然得用神力啦,而且必须对最大的国家用神力。然后二分一下<=k次数的神力可以访问多少个国家。
答案的思路其实更为简便,一直往后找并插入到优先队列,当哪一次生命力不够的时候,开始弹出最大值,并记录使用神力的次数,当次数用完则输出当前游玩过的城市。其实从我的错误思路出发应该得走到这的。
代码:
//以下是我的代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m,k;
int a[200005];
int js[200005];
priority_queue<int>c;
bool cmp(pair<int,int>x,pair<int,int>y){
if(x.first == y.first)return x.second < y.second;
else return x.first > y.first;
}
signed main() {
cin >> n >> m >> k;
int res = 0;
int z = 0;
int ans = 0;
for (int i = 1; i <= n; i++) {
cin >> a[i];
c.push(a[i]);
res += a[i];
while(res >= m) {
z++;
res-=c.top();
c.pop();
}
js[i] = z;
}
// for (int i = 1; i <= n; i++) {
// cout << js[i] << endl;
// }
int l = 0,r = n;
while (l < r) {
int mid = (l+r+1)/2;
if(js[mid] <= k)l = mid;
else r = mid-1;
}
cout << l;
return 0;
}
//以下是给出的代码
#include<iostream>
#include<queue>
using namespace std;
typedef long long ll;
const int N = 2e5+5;
ll n,m,k;
ll a[N];
priority_queue<ll,vector<ll>,greater<ll>>q;
signed main() {
cin >> n >> m >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
ll sum = 0;
for (int i = 1; i <= n; i++) {
q.push(a[i]);
if(q.size() > k) {
sum+=q.top();
q.pop();
}
if(sum >= m) {
cout << i-1 << endl;
return 0;
}
}
cout << n << endl;
return 0;
}
3. 并查集
int find(int x) {
if(fa[x] == x)return x;
return fa[x] = find(fa[x]);
}
void merge(int x,int y) {
int xx = find(x),yy = find(y);
if( xx != yy)fa[xx] = y;
}
signed main() {
for (int i = 0; i <= n; i++) {
fa[i] = i;
}
return 0;
}
<1>(食物链)
食物链
题解:
a吃b,b吃c,c吃a,n个动物相应编号,现在给出k句话,有真有假,输出有几句假话。当这句话与前面冲突时,它就是假话。
每个动物其实都可以是a,b,c中的任意一个,这是一个相对关系,所以每次往里插入关系时,只要不与前面相冲突,就连接这俩个关系,否则continue。
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,k;
int fa[200005];
int find(int x) {
return fa[x] == x? x: fa[x] = find(fa[x]);
}
void merge (int a,int b) {
fa[find(a)] = find(b);
}
signed main() {
cin >> n >> k;
for (int i = 1; i <= 3*n; i++) {
fa[i] = i;
//fa[i]表示i是a,fa[i+n]代表i是b,fa[i+2*n]代表i是c
}
int cnt = 0;
for (int i = 1; i <= k; i++) {
int op,x,y;
cin >> op >> x >> y;
if( x > n || y > n){
cnt++;
continue;
}
if(op == 1) {
if(find(x) == find(y+n) || find (x) == find(y+2*n)){
//x是a,y是b或c
cnt++;
}
else {
merge(x,y);
merge(x+n,y+n);
merge(x+2*n,y+2*n);
//这俩者等价
}
}
else {
if(find(x) == find(y) || find (x) == find(y+2*n)){
cnt++;
}
else {
merge(x,y+n);
merge(x+n,y+2*n);
merge(x+2*n,y);
}
}
}
cout << cnt << endl;
return 0;
}
4. 搜索与搜索剪枝(DFS)
深度遍历就是找到最底下再返回一层找别的,以此类推
如果是需要输出此时所储存的,多开一个数组记录即可
void dfs(int x) {
if(x > n){
for (int i = 1; i <= n; i++) {
cout << a[i] << ' ';
}
cout << endl;
return ;
}
for (int i = 1; i <= n; i++) {
if(vis[i])continue;
//已经用过这个数字
a[x] = i;
vis[i] = 1;
dfs(x+1);
下一个位置
a[x] = 0;
vis[i] = 0;
//状态返回
}
}
<1>(模拟战役)
题解:
齐齐和司机在玩大炮,齐齐先手交替出手,齐齐可以准确看到对方的大炮部署,击中大炮时会在3*3爆炸,会持续波及。司机只能在齐齐出手后反击相应的大炮。
先用dfs寻找联通块,对俩者的联通块大小分别进行排序,一直用齐齐最小的联通块攻击对方最大的,最终可以保留最多的本方大炮。
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int m,cnt;
char a[6][102];
char b[6][102];
bool st[6][102];
int ra[500];
int rb[500];
int dx[] = {-1,0,1,-1,0,1,-1,0,1};
int dy[] = {-1,-1,-1,0,0,0,1,1,1};
int res1 = 0,res2 = 0;
void dfs(int x,int y, char c[6][102]) {
st[x][y] = 1;
//记录这个大炮已经被归入某一个联通块
cnt++;
for (int i = 0; i < 9; i++) {
if(x+dx[i] < 0 || x+dx[i] >= 4 || y+dy[i] < 0 || y+dy[i] >= m)continue;
if(c[x+dx[i]][y+dy[i]] == '*' && !st[x+dx[i]][y+dy[i]]) {
dfs(x+dx[i],y+dy[i],c);
//继续查找它是否波及到别的大炮
}
}
return ;
}
void solve(){
memset(st,0,sizeof st);
for (int i = 0; i < 4; i++) {
for (int j = 0; j < m; j++) {
cnt = 0;
if(a[i][j] == '*' && !st[i][j]) {
dfs(i,j,a);
ra[res1++] = cnt;
//这个联通块大小
}
}
}
memset(st,0,sizeof st);
for (int i = 0; i < 4; i++) {
for (int j = 0; j < m; j++) {
cnt = 0;
//每次都要记得归零
if(b[i][j] == '*' && !st[i][j]) {
dfs(i,j,b);
rb[res2++] = cnt;
}
}
}
}
signed main() {
cin >> m;
for (int i = 0; i < 4; i++) {
for (int j = 0; j < m; j++) {
cin >> a[i][j];
}
}
for (int i = 0; i < 4; i++) {
for (int j = 0; j < m; j++) {
cin >> b[i][j];
}
}
solve();
sort(ra,ra+res1,greater<int>());
sort(rb,rb+res2);
if(res1 > res2) {
cout << -1;
return 0;
}
int ans = 0;
if(res1 == 0)res1 = 1;
for (int i = res1-1; i < res2; i++) {
ans+=rb[i];
}//齐齐剩下的较大的联通块大小之和
cout << ans;
return 0;
}
<2>(Linear Probing)
非典型dfs,稍微辨析一下。
题解:
给定一个长度为n且每个数都是-1的数组a,q次询问,每个询问输入t和x,如果t=1,令h=x%n,在数组a中找到第一个为-1的数字,变成x。如果t=2,直接输出a[x%n]。
可以考虑到,在寻找第一个为-1的数字时会出现反复重复遍历同一段数组的情况,所以定义一个to数组,每次直接跳跃过去已经确定过都不是-1的数组,具体见注释。
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1048576;
int a[1050000];
int q,lq;
int to[1050000];
void dfs(int t,int x) {
if(a[t] == -1) {
lq = t;
//它的下一个不为-1,标记一下
a[t] = x;
return ;
//开始返回啦
}
dfs(to[t],x);
to[t] = lq;
//在回来的路上把沿途的to都直接赋值到最后一个lq
}
signed main() {
cin >> q;
memset(a,-1,sizeof a);
for (int i = 0; i < N; i++) {
to[i] = i+1;
}to[N-1] = 0;
while (q--) {
int t,x;
cin >> t >> x;
if(t == 1) {
int h = x;
dfs(h%N,x);
}
else if(t == 2) cout << a[x%N] << endl;
}
return 0;
}
5.搜索与搜索剪枝(BFS)
先搜索一层再搜索下一层
由每个点向四处走,对于没走过的节点附上步数,示意图处无法走到终点
//一般利用队列实现
int n,m;
typedef pair<int,int> PII;
queue<PII>q;
int a[10][10];
int dis[10][10];
int dx[] = {-1,1,0,0};
int dy[] = {0,0,-1,1};
void bfs(int x,int y) {
memset(dis,-1,sizeof dis);
dis[x][y] = 0;
q.push({x,y});
while(!q.empty()) {
PII tmp = q.front();
for (int i = 0; i < 4; i++) {
int lx = tmp.first+dx[i];
int ly = tmp.second+dy[i];
if(ly < 0 || lx < 0 || lx >= n || lx >= m) continue;
if(dis[lx][ly] == -1) {
dis[lx][ly] = dis[tmp.first][tmp.second]+1;
q.push({lx,ly});
}
}
q.pop();
}
cout << dis[n-1][m-1];
}
<1>(Jelly)
老规矩放一道板子题。
题解:
从(1,1,1)走到(n,n,n),‘*’不能走,求最短路径。
从第一个位置开始向四周没有被标记的位置扩散行走。具体见注释。
代码:
#include<bits/stdc++.h>
using namespace std;
int n;
char a[102][105][102];
int di[] = {0,0,-1,1,0,0};
int dj[] = {0,0,0,0,-1,1};
int dk[] = {-1,1,0,0,0,0};
//几个既定方向位置
int vis[105][105][105];
//标记是否走过
struct ty{
int x;
int y;
int z;
};//三维数组重新定义一下。
queue<ty>q;
void bfs() {
memset(vis,-1,sizeof vis);
vis[1][1][1] = 1;
//第一块果冻
q.push({1,1,1});
while(!q.empty()) {
//全部走一遍八
ty tmp = q.front();
for (int i = 0; i < 6; i++) {
int lx = tmp.x+di[i];
int ly = tmp.y+dj[i];
int lz = tmp.z+dk[i];
if(lz <= 0 || lx <= 0 || ly <= 0 || lx > n || ly > n || lz > n) continue;
if(vis[lx][ly][lz] == -1 && a[lx][ly][lz] != '*') {
vis[lx][ly][lz] = vis[tmp.x][tmp.y][tmp.z]+1;
q.push({lx,ly,lz});
//来来来继续
}
}
q.pop();
//这个走完了
}
cout << vis[n][n][n] << endl;
//输出即可
}
signed main() {
cin >> n;
for (int i = 1 ; i <= n; i++) {
for (int j = 1; j <= n; j++) {
for (int k = 1; k <= n; k++) {
cin >> a[i][j][k];
}
}
}
bfs();
}
三、总结
_int128是特别大的整数数据类型
signed main()必须return 0,不然会tle
map的查询和清空时间会比vector短,在多数据里考虑用map储存
nan的意思是not a number,可能是除0错误,对负数开平方等问题。
第二周总体打得飘忽不定而且没有一次比赛特别突出,算法课学起来更为吃力,搜索掌握得不太好。状态其实也没第一周好,老想划水(小小声)下周调整,继续加油。