模板题练习(第一周)
栈(Stack) 队列 (Queue)
题目:B3614
题目:B3616
直接调用 C++ STL函数库里面的 stack
和 queue
,分别代表栈和队列。
Stack
-
push : 将一个压入栈顶,形如
y.push(x)
-
pop :将一个元素弹出栈,当且仅当栈非空的情况下,形如
y.pop()
-
empty : 返回当前这个栈是否为空,形如
y.empty()
-
top : 返回栈顶元素(不弹出),当且仅当栈非空的情况下,形如
y.top()
-
size : 返回当前栈的元素数量,形如
y.size()
Queue
-
push : 将一个加入队列,形如
y.push(x)
-
pop :将一个元素弹出队列,当且仅当队列非空的情况下,形如
y.pop()
-
empty : 返回当前这个栈队列是否为空,形如
y.empty()
-
front : 返回队首元素(不弹出),当且仅当队列非空的情况下,形如
y.top()
-
size : 返回当前队列的元素数量,形如
y.size()
分别对应的代码如下:
#include <bits/stdc++.h>
#define ull unsigned long long
using namespace std;
main() {
cin.tie(0);
ios::sync_with_stdio(false);
ull t;
cin >> t;
while (t--) {
ull n;
cin >> n;
stack<ull> st;
while (n--) {
string s;
cin >> s;
if (s == "push") {
ull k;
cin >> k;
st.push(k);
} else if (s == "pop") {
if (st.empty())
cout << "Empty\n";
else {
st.pop();
}
} else if (s == "query") {
if (st.empty())
cout << "Anguei!\n";
else {
cout << st.top() << endl;
}
} else if (s == "size") {
cout << st.size() << endl;
}
}
}
return 0;
}
#include <bits/stdc++.h>
using namespace std;
int main() {
queue<int> q;
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
if (n == 1) {
int m;
cin >> m;
q.push(m);
}
if (n == 2) {
if (q.empty())
cout << "ERR_CANNOT_POP\n";
else
q.pop();
}
if (n == 3) {
if (q.empty())
cout << "ERR_CANNOT_QUERY\n";
else
cout << q.front() << "\n";
}
if (n == 4) {
cout << q.size() << endl;
}
}
return 0;
}
链表(List)
题目:B3631
与上两个不同,c++里面没有专门为链表设计STL,所以我们自己用struct
设计。
首先在定义的时候,先考虑我们应该设计什么变量。
第一,需要当前元素的值对吧。第二,需要下一个元素的位置(也就是地址)对吧。由此可得:
struct Node{
int val;
Node next;//这里使用Node来作属性,主要是因为我们需要的是地址
//所以我们不能用其它属性,类似一个重复递归(bushi)。
};
现在根据题意设计函数:
-
链接函数:x的next相等于y的next,y的next等于y的值。
-
删除函数:直接向简单点,把我们需要删除元素的上一个元素的指向直接指向这个元素的下一个元素,即可完成删除,不需要对这个元素做其他的处理,这就是相较于数组,可以做到
O(1)
的好处。 -
添加函数:显而易见,直接在末尾函数的下一个就是我们需要的值。
由此可得代码:
#include <bits/stdc++.h>
using namespace std;
struct Node {
int value;
Node *next;
Node(int v) : value(v), next(NULL) {}
};
Node *li[1000001] = {NULL};
int main() {
int q;
cin >> q;
li[1] = new Node(1);
li[1]->next = NULL;
while (q--) {
int op;
cin >> op;
if (op == 1) {
int x, y;
cin >> x >> y;
Node *xn = li[x];
Node *yn = new Node(y);
yn->next = xn->next;
xn->next = yn;
li[y] = yn;
} else if (op == 2) {
int x;
cin >> x;
Node *xn = li[x];
cout << ((xn->next) ? xn->next->value : 0) << endl;
} else if (op == 3) {
int x;
cin >> x;
Node *xn = li[x];
if (xn->next) {
Node *yn = xn->next;
xn->next = yn->next;
li[yn->value] = NULL;
delete yn;
}
}
}
return 0;
}
简单二叉树练习
题目:P1305
这道题我们可以通过构造二叉树的一种思路来想。
那么前序遍历。
#include <bits/stdc++.h>
using namespace std;
struct Node {
int value;
Node *next;
Node(int v) : value(v), next(NULL) {}
};
Node *li[1000001] = {NULL};
int main() {
int q;
cin >> q;
li[1] = new Node(1);
li[1]->next = NULL;
while (q--) {
int op;
cin >> op;
if (op == 1) {
int x, y;
cin >> x >> y;
Node *xn = li[x];
Node *yn = new Node(y);
yn->next = xn->next;
xn->next = yn;
li[y] = yn;
} else if (op == 2) {
int x;
cin >> x;
Node *xn = li[x];
cout << ((xn->next) ? xn->next->value : 0) << endl;
} else if (op == 3) {
int x;
cin >> x;
Node *xn = li[x];
if (xn->next) {
Node *yn = xn->next;
xn->next = yn->next;
li[yn->value] = NULL;
delete yn;
}
}
}
return 0;
}
List 类
没有直接模拟队列,而是使用List
类。
请看Code。
#include <bits/stdc++.h>
using namespace std;
using Iter = list<int>::iterator;
const int maxN = 1e5 + 10;
Iter pos[maxN];
list<int> queList;
bool erased[maxN];
int N;
void buildQueue() {
queList.push_front(1);
pos[1] = queList.begin();
for (int i = 2; i <= N; i++) {
int k, p;
cin >> k >> p;
if (p == 0) {
pos[i] = queList.insert(pos[k], i);
} else {
auto nextIter = next(pos[k]);
pos[i] = queList.insert(nextIter, i);
}
}
int M;
cin >> M;
for (int x, i = 1; i <= M; i++) {
cin >> x;
if (!erased[x]) {
queList.erase(pos[x]);
}
erased[x] = true;
}
}
int main() {
cin >> N;
buildQueue();
bool first = true;
for (int x : queList) {
if (!first)
putchar(' ');
first = false;
cout << x;
}
putchar('\n');
return 0;
}
二分查找
二分查找,是分治算法的一种变形。
也想一种递归,我们可以通过确定左边界或者右边界来求解。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<int> arr(n + 1);
for (int i = 1; i <= n; ++i) {
cin >> arr[i];
}
vector<int> results;
results.reserve(m);
for (int i = 0; i < m; ++i) {
int q;
cin >> q;
int left = 1, right = n;
while (left < right) {
int mid = left + (right - left) / 2;
if (arr[mid] >= q) {
right = mid;
} else {
left = mid + 1;
}
}
if (arr[left] == q) {
results.push_back(left);
} else {
results.push_back(-1);
}
}
for (size_t i = 0; i < results.size(); ++i) {
if (i > 0) {
cout << ' ';
}
cout << results[i];
}
cout << '\n';
return 0;
}
二分答案
二分答案,简单来说就是在一个区间内通过二分找答案。
题目:P1577
二分最重要的是确定左右边界,那么显而易见:
左边界为 1 ,右边界为 最大的长度 。
现在我们只需要在这个区间内做二分,然后通过check
函数判断是否满足条件,如果满足就看能否缩小,不满足就增大。
下面上Code:
#include <bits/stdc++.h>
using namespace std;
int n, k;
double a[10005], l, r, mid;
char s[100];
inline bool check(double x) {
int tot = 0;
for (int i = 1; i <= n; i++)
tot += floor(a[i] / x);
return tot >= k;
}
int main() {
cin >> n >> k;
for (int i = 1; i <= n; i++)
cin >> a[i], r += a[i];
while (r - l > 1e-4) {
mid = (l + r) / 2;
if (check(mid))
l = mid;
else
r = mid;
}
sprintf(s + 1, "%.3f", l);
s[strlen(s + 1)] = '\0';
printf("%s", s + 1);
return 0;
}
贪心
很简单的贪心模板,就算出每重量单位的价格,然后取最大,之后就按从大到小来就可以了。
#include <bits/stdc++.h>
#define Robin() ios::sync_with_stdio(0)
#define ld long double
using namespace std;
struct cw {
int m, v;
double s;
}arr[110];
bool cmp(cw a, cw b) {
return a.s > b.s;
}
int main() {
int n, t;
cin >> n >> t;
for (int i = 0; i < n; ++i) {
cin >> arr[i].m >> arr[i].v;
arr[i].s = (double)arr[i].v / arr[i].m;
}
sort(arr, arr + n, cmp);
double ans = 0.0;
for (int i = 0; i < n && t > 0; ++i) {
if (arr[i].m <= t) {
ans += arr[i].v;
t -= arr[i].m;
} else {
ans += t * arr[i].s;
t = 0;
}
}
printf("%.2lf\n", ans);
return 0;
}
DFS
对于如果不是求最短路径,所以我们可以选择 DFS 或 BFS 两种方案。
题目:B3625
对于这种判断一张图上的题目,DFS函数要先设置横纵轴坐标,然后递归上下左右四个方向,如果经过一轮搜索,目标点没有被找到,那么就返回NO
,当然找到了自然会标记的。
#include<bits/stdc++.h>
using namespace std;
int n, m;
char arr[110][110];
int dx[] = {0, 1, 0, -1, 0};
int dy[] = {0, 0, 1, 0, -1};
bool flag = false;
void dfs(int x, int y) {
if (x == n && y == m) {
flag = true;
return;
}
arr[x][y] = '#';
for (int i = 1; i <= 4; ++i) {
int nx = x + dx[i];
int ny = y + dy[i];
if (nx < 1 || nx > n || ny < 1 || ny > m) continue;
if (arr[nx][ny] == '.') {
dfs(nx, ny);
if (flag) return;
}
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
cin >> arr[i][j];
dfs(1, 1);
cout << (flag ? "Yes" : "No") << endl;
return 0;
}
BFS
还有一个思路,使用BFS思路。
我们都知道,BFS可以通过每一种可能性都同步前进,所以在第一个获取到答案的方案中就是我们需要的答案。
#include <bits/stdc++.h>
using namespace std;
int n, m;
char maze[110][110];
int dx[] = {-1, 1, 0, 0};
int dy[] = {0, 0, -1, 1};
bool bfs() {
queue<pair<int, int> > q;
q.push(make_pair(1, 1));
maze[1][1] = '#';
while (!q.empty()) {
int x = q.front().first;
int y = q.front().second;
q.pop();
if (x == n && y == m) return true;
for (int i = 0; i < 4; ++i) {
int nx = x + dx[i];
int ny = y + dy[i];
if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && maze[nx][ny] == '.') {
maze[nx][ny] = '#';
q.push(make_pair(nx, ny));
}
}
}
return false;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
cin >> maze[i][j];
if (bfs()) cout << "Yes" << endl;
else cout << "No" << endl;
return 0;
}
DP
可以知道动态规划也是可做这道题的,因为每个位置都无后效性,与动态规划相符。
状态转移方程:
$ dp[i][j] = dp上一个方向+1 $
如果最后的节点没有为0,证明当前节点可到达。
以此来编出程序(goto
为断点跳出):
#include <bits/stdc++.h>
using namespace std;
int n, m;
char arr[110][110];
bool dp[110][110];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
cin >> arr[i][j];
memset(dp, 0, sizeof(dp));
dp[1][1] = (arr[1][1] == '.');
bool flag;
do {
flag = false;
bool vis[110][110];
memcpy(vis, dp, sizeof(dp));
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (arr[i][j] == '.' && !vis[i][j]) {
if (vis[i-1][j] || vis[i+1][j] || vis[i][j-1] || vis[i][j+1]) {
dp[i][j] = true;
flag = true;
if (i == n && j == m) goto position;
}
}
}
}
} while (flag);
position : cout << (dp[n][m] ? "Yes" : "No") << endl;
return 0;
}
DFS
题目:P1596
这里的思路就是每个水洼都用DFS全部“填充”,然后每一个水洼记录就可以了。
#include <bits/stdc++.h>
#define Robin() ios::sync_with_stdio(0)
#define ld long double
using namespace std;
int n, m, ans = 0;
char arr[1010][1010];
int dx[] = {0, -1, -1, -1, 0, 1, 1, 1, 0};
int dy[] = {0, -1, 0, 1, 1, 1, 0, -1, -1};
void dfs(int x, int y) {
arr[x][y] = '.';
for (int i = 1; i <= 8; ++i) {
int nx = x + dx[i];
int ny = y + dy[i];
if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && arr[nx][ny] == 'W') {
dfs(nx, ny);
}
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
cin >> arr[i][j];
}
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (arr[i][j] == 'W') {
dfs(i, j);
ans++;
}
}
}
cout << ans << endl;
return 0;
}
洪水填充算法
洪水填充算法,显而易见就是将水倒进一个容器里面,这种思路有点像BFS。
题目:P1506
这道题的题目背景跟洪水填充也有点相似,我们直接从周边“倒洪水”,然后看看有没有没被覆盖的,剩下的判断连通块就是上一题代码内容了好吧,这里其实不是算连通块,而是有空位就累加就可以了。
#include <bits/stdc++.h>
#define Robin() ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
#define ld long double
using namespace std;
int n, m, ans = 0;
char arr[510][510];
//int dx[] = {0, -1, -1, -1, 0, 1, 1, 1, 0};
//int dy[] = {0, -1, 0, 1, 1, 1, 0, -1, -1};
int dx[] = {0, 0, -1, 0, 1};
int dy[] = {0, 1, 0, -1, 0};
void dfs(int x, int y) {
arr[x][y] = '*';
for (int i = 1; i <= 4; ++i) {
int nx = x + dx[i];
int ny = y + dy[i];
if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && arr[nx][ny] == '0') {
dfs(nx, ny);
}
}
}
void dfs2(int x, int y) {
arr[x][y] = '*';
for (int i = 1; i <= 4; ++i) {
int nx = x + dx[i];
int ny = y + dy[i];
if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && arr[nx][ny] == '0') {
dfs2(nx, ny);
}
}
}
int main() {
Robin();
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
cin >> arr[i][j];
}
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if ( (i == 1 || j == 1 || i == n || j == n) && arr[i][j] != '*') {
dfs(i, j);
//cout << i << " " << j << endl;
}
}
}/*
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
cout << arr[i][j];
}
cout << endl;
}*/
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (arr[i][j] == '0') {
ans++;
}
}
}
cout << ans << endl;
return 0;
}
Garsia-Wachs 算法 或者 动态规划 「区间」
你说得对,确实偏离了初衷,但还是有这道题的算法。
如果需要了解,可以看下面这个。
👇👇👇主要就是一个贪心思路,找到了石子合并的性质。
Garsia-Wachs
那么我们还是主要讨论区间DP。
方法思路
-
状态定义:定义dp[i][j]为合并第i堆到第j堆石子的最小代价。
-
前缀和优化:使用前缀和数组快速计算区间和,减少重复计算。
-
状态转移:将区间[i, j]拆分为两个子区间[i, k]和[k+1, j],遍历所有可能的k,找到最小合并代价。
-
动态规划顺序:按区间长度从小到大递推,确保计算大区间时所需的子区间已计算完成。
解决代码
#include <bits/stdc++.h>
#define Robin() ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
#define ld long double
using namespace std;
const int N = 305;
const int INF = 20251029;//某个特别数字,不用在意
int n, m[N], s[N], dp[N][N];
int main() {
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> m[i];
}
s[0] = 0;
for (int i = 1; i <= n; ++i) {
s[i] = s[i - 1] + m[i];
}
for (int i = 1; i <= n; ++i) {
for (int j = i; j <= n; ++j) {
dp[i][j] = (i == j) ? 0 : INF;
}
}
for (int len = 2; len <= n; ++len) {
for (int i = 1; i + len - 1 <= n; ++i) {
int j = i + len - 1;
for (int k = i; k < j; ++k) {
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + s[j] - s[i - 1]);
}
}
}
cout << dp[1][n] << endl;
return 0;
}
BFS2
题目:P1746
这个也是经典BFS最短路算法的题目。
我们可以用STL队列来解决这个问题,首先设置队列,对于每个新弹出的队列,找出它四个方向的位置再重复操作。
代码如下:
#include <bits/stdc++.h>
#define Robin() ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
using namespace std;
queue<int> q ;
int n, x, y, a, b ;
int flag[2100][2100], l[2100][2100] ;
void check( int c, int d, int s, int t ) {
if ( l[s + c][t + d] == 1 )
return ;
if ( flag[s + c][t + d] == -1 ) {
if ( ( s + c > 0 && s + c <= 1200 ) && ( t + d > 0 && t + d <= 1200 ) ) {
q.push(s + c) ;
q.push(t + d) ;
flag[s + c][t + d] = flag[s][t] + 1 ;
}
}
return ;
}
int main() {
Robin();
memset( flag, -1, sizeof(flag)) ;
cin >> n;
for ( int i = 1 ; i <= n ; i ++ ) {
string tmp;
cin >> tmp;
for ( int j = 1 ; j <= n ; j++ )
l[i][j] = tmp[j - 1] - '0' ;
}
cin >> x >> y >> a >> b;
q.push(x) ;
q.push(y) ;
flag[x][y] = 0 ;
while (!q.empty()) {
int s = q.front() ;
q.pop() ;
int t = q.front() ;
q.pop() ;
if ( s == a && t == b ) {
cout << flag[s][t];
return 0 ;
}
check(1, 0, s, t) ;
check(0, 1, s, t) ;
check(-1, 0, s, t) ;
check(0, -1, s, t) ;
}
return 0 ;
}
DFS&BFS
这一次是关于树的遍历,题目需要我们使用BFS或者DFS分别作答题目所需要我们的方式。
代码:
#include <bits/stdc++.h>
#define Robin() ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
using namespace std;
int n, m;
set<int>e[100009];
bool vis[100009];
void dfs(int x = 1) {
if (vis[x])
return;
vis[x] = 1;
printf("%d ", x);
for (int v : e[x])
dfs(v);
}
void bfs() {
queue<int>q;
q.push(1);
while (!q.empty()) {
int x = q.front();
q.pop();
if (vis[x])
continue;
vis[x] = 1;
printf("%d ", x);
for (int v : e[x])
q.push(v);
}
}
int main() {
Robin();
cin >> n >> m;
while (m--) {
int u = 0, v = 0;
cin >> u >> v;
e[u].insert(v);
}
dfs();
printf("\n");
memset(vis, 0, sizeof(vis));
bfs();
printf("\n");
return 0;
}
差分
题目:P2367
这种题第一眼让人以为是线段树,仔细分辨一下其实发现是差分。
在读入的时候先存一下差分数组,然后按照查找的三个数字进行查找。
最后输出的时候找到最小值,然后输出即可。
代码如下:
(另外,这里使用快速读入输出模板是上一题代码)
(实际上并没有什么用)(不如用夜色流光溢彩bushi)
#include <bits/stdc++.h>
#define r(x) x=read()
#define w(x) write(x)
using namespace std;
typedef long long ll;
namespace FastIO {const int BUFSIZE = 1 << 20;char buf[BUFSIZE], *p1 = buf, *p2 = buf;inline char gc() {if (p1 == p2){p2 = (p1 = buf) + fread(buf, 1, BUFSIZE, stdin);}return p1 == p2 ? EOF : *p1++;}}
using FastIO::gc;
ll read() {ll k = 0, f = 1;char c = gc();while (c < '0' || c > '9') {if (c == '-'){f = -1;}c = gc();}while (c >= '0' && c <= '9') {k = k * 10 + (c - '0'),c = gc();}return k * f;}
void write(ll x) {if (x == 0) {putchar('0');return;}if (x < 0) {putchar('-');x = -x;}char buffer[20];int ptr = 0;while (x) {buffer[ptr++] = x % 10 + '0',x /= 10;}while (ptr--){putchar(buffer[ptr]);}}
int n, q, d[int(5e6+5)], a, minn, x, y, z;
int main(){
r(n),r(q);
for(int i = 1; i <= n; i++) r(d[i]);
for(int i = n; i > 1; i--) d[i] -= d[i - 1];
while(q--){
r(x),r(y),r(z);
d[x] += z, d[y + 1] -= z;
}
minn = d[1];
for(int i = 2; i <= n; i++){
d[i] += d[i - 1];
if(d[i] < minn) minn = d[i];
}
w(minn);
return 0;
}
第一周总结
这一周,我的主要练习方向是 模板 类型题目,主要类型有:搜索(包括:DFS BFS 洪水填充 等) 动态规划(包括:背包 状压 划分 等) 经典(包括:快速幂 并查集 等) STL(包括:栈 队列 链表 等) 分治(包括:二分查找 二分答案 等)其它(包括:贪心 二叉树 等)
通过这些题目都极大的提升了我的能力(这是次要的),重要的是,模板题的适用范围很广,是非常有用的。
目前总共写了 17 篇题解(实际解决题目可能大于这个数字),换算下来,即周一到周五每天做2 ~ 3道题,周末做3 ~ 4道题。
同时还完成了 代数 的第一章节的习题。
评价一下:这个进度(效率)如何?
A.非常好
B.A
题目题单:This
下一周的目标&任务:
数学:完成 数论 的第一章习题
信奥:完成 普及算法 习题
(由于题目难度跨度较大,所以目标不要求太多,在原来一周基础上减少 2 题即可)
以上就是第一周练习总结。