A-6的个数
题意
2021中,出现了多少个数字6
思路
for循环枚举对于每个数分解之后贡献答案就可以了
代码
#include <bits/stdc++.h>
using namespace std;
int main() {
int num = 0;
for(int i = 1; i <= 2021; i++) {
int k = i;
while(k) {
if(k % 10 == 6) num++;
k /= 10;
}
}
printf("%d\n", num);
}
B-小明的作业
题意
统计字符串中有多少个 “警告” (aw 或者 wa)和 “错误” (wa…wa 或者 aw…aw)
思路
遍历每个字符,如果是 a 或者 w,在此之后就有可能是对答案有贡献的一段了,那么对于这一段 a… 后者 w… 进行统计和判断 (我用vector来统计的),将第一个字符存入容器,然后继续往后推进,对于容器最后一个字符与我当前遍历到的字符做一下对比,比如vector中存的是 awa 了,那么如果当前遍历到的字符是w,那么就继续往里面存,如果是别的字符,就明显不属于这一段了;然后开始结算,vector中的字符串长度是 2 或者 3 的话就属于警告类型的,有人或许会疑惑3为什么是警告类型,好好想想吧。大于3的就是错误类型(不需要奇偶判断哦,跟前面那个一样的道理),结算完成后,就开始新一轮的判断,就又从开头开始,直到字符串遍历结束。
代码
#include <bits/stdc++.h>
using namespace std;
string s;
int main() {
cin >> s;
int a = 0, b = 0;
int pos = 0, n = s.length();
while(pos < n) {
if(s[pos] == 'a' || s[pos] == 'w') { // 统计开始
vector <char> g; // 定义容器
g.push_back(s[pos++]); // 把11行的字符放进来
while((g[g.size()-1] == 'a' && s[pos] == 'w') || (g[g.size()-1] == 'w' && s[pos] == 'a')) { // 往后推进 a后必接w w后必接a
g.push_back(s[pos++]); // 继续放进容器
}
if(g.size() <= 3 && g.size() > 1) a++; // 答案结算
else if(g.size() > 3) b++;
g.clear();
}
else pos++; // 不属于 a w 就继续推进
}
printf("%d\n%d\n", a, b);
}
C-斐波那契
题意
连续13对斐波那契数列相乘的倒数之和
思路
求出最简分数的形式,很容易想到小学时期的技能:通分->约分 的组合技,这题也确实就是这样写的,模拟一下通分和约分过程就可以了。
首先把斐波那契数列前14项求出来,这样才会出现连续13对。通分的过程最粗暴的做法就是这13个数直接乘起来,不过这样太大了,我们就搞一点点优化,就是求他们的最小公倍数。这样所有数相加的分母就搞出来了,每一个分子就是 分母/当前这个值。把这些数加起来就得到了最后没有约分的答案,约分就除以最大公约数就可以了。这个题可以学一下 __int128 的用法,因为虽然是只有13对数,但是毕竟是斐波那契数列呀,会爆 long long 的。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll f[20];
vector <ll> g;
ll lcm(ll a, ll b) {
return (__int128)(a*b)/__gcd(a, b);
}
int main() {
f[1] = 1; f[2] = 1;
for(int i = 3; i <= 14; i++) f[i] = f[i-1] + f[i-2]; // 预处理斐波那契数列
for(int i = 2; i <= 14; i++) {
g.push_back(f[i]*f[i-1]); // 相邻数的乘积
}
__int128 lc = lcm(g[0], g[1]); // 求出所有数的最小公倍数
for(int i = 2; i < g.size(); i++) {
lc = lcm(lc, g[i]);
}
__int128 sum = 0;
for(int i = 0; i < g.size(); i++) {
sum += lc / g[i]; // lc / g[i] 就是每一个位置上的分子的值; sum 维护分子之和
}
__int128 gcd = __gcd(sum, lc);
sum /= gcd; lc /= gcd;
cout << (ll)sum << "/" << (ll)lc << endl;
}
D-数列重组
题意
求一组数字有多少种排列方式使得存在一种分法,把这组数分为连续的3部分,每一部分要么是 不严格递增 或者 不严格递减 (不严格就是前后相等也可以 1、1、2、3 就是不严格递增 4、4、4、3 是不严格递减)
思路
因为这组数只有10个数,暴力枚举每一种排法时间也是够的 枚举时间小于 O(10!) 因为这里面有相同的数,相同的数互换位置视为同一种排列顺序。这里可以学一下 next_permutation 这个内置进行全排列的函数
然后就是对于每一种排列顺序,我们又可以暴力的枚举分为哪三个连续块,可以用两层循环来实现 第一段 i : [ 0 , 7 ] i : [0, 7] i:[0,7] 第二段 j : [ i + 1 , 8 ] j : [i+1, 8] j:[i+1,8] 第三段 k : [ j + 1 , 9 ] k : [j+1, 9] k:[j+1,9] 再判断每一段是否符合要求就可以了。
代码
#include <bits/stdc++.h>
using namespace std;
int a[] = {2,5,3,6,3,6,7,3,7,8};
bool check(int l, int r) { // 简单易懂的判断函数
bool ok1 = 1, ok2 = 1;
for(int i = l+1; i <= r; i++) {
if(a[i-1] < a[i]) {
ok1 = 0;
break;
}
}
for(int i = l+1; i <= r; i++) {
if(a[i-1] > a[i]) {
ok2 = 0;
break;
}
}
return ok1 || ok2; // 要么升序 要么降序
}
int main() {
int ans = 0;
sort(a, a+10); // 使得字典序最小 然后好用全排列函数
do { // 全排列函数的固定搭配
bool ok = 0;
for(int i = 0; i < 8; i++) { // 枚举第一段
for(int j = i+1; j < 9; j++) { // 枚举第二段
if(check(0, i) && check(i+1, j) && check(j+1, 9)) { // 第三段直接可以算出来 然后直接判断三段区间是否合法
ans++;
ok = 1;
break;
}
}
if(ok) break;
}
} while(next_permutation(a, a+10));
printf("%d\n", ans);
}
E-三角形个数
题意
给出等边三角形的层数,求出图形中有多少个等边三角形。
思路
找规律。我们可以发现三角形的种类分为正着放的和倒着放的,所以可以试着分开讨论一下
我们先讨论正着放的三角形:
第一行:1个 tot : 1=1
第二行:增加了 2个边长 1 的 、1个边长 2 的 tot : 1+2=3
第三行:增加了 3个边长 1 的 、2个边长 2 的 、1个边长 3 的 tot : 1+2+3=6
…
这个规律就就比较明显了,对于第四行一定是 1+2+3+4=10 ,总结起来就是对于第 i 行的三角形对答案的贡献就是 ∑ 1 i = ( 1 + i ) ∗ i / 2 \sum_{1}^{i} = (1+i)*i/2 ∑1i=(1+i)∗i/2
再来讨论倒着放的三角形:
第一行:0个
第二行:1个
第三行:2个
第四行:3 + 1 = 4个
第五行:4 + 2 = 6个
写到这里其实规律就有点浮出水面了,是不是就是对于第i行对答案的贡献就是 ( i − 1 ) + (i-1) + (i−1)+ 前两行对于答案的贡献,第四行就是 3 + 第1行的贡献 ,第五行就是 4 + 第3行的贡献。可以自己画一下第六行的贡献。这个拿 前缀和 搞一下就可以了,20级对于这个名词不应该陌生了,陌生的面壁去,然后再花一分钟学一下。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 1e9+7;
const int N = 20210411;
ll dao[N + 10];
int main() {
dao[1] = 0, dao[2] = 1;
for(int i = 3; i <= N; i++) { // 前缀和维护倒三角形的数量
dao[i] = (dao[i-2] + (i-1)) % mod;
}
ll ans = 0;
for(int i = 1; i <= N; i++) {
ans = (ans + 1ll * (1 + i) * i / 2) % mod; // 正三角形的贡献
ans = (ans + dao[i]) % mod; // 倒三角形的贡献
}
printf("%lld\n", ans);
}
F-字符串
题意
输入n个字符串,问有多个字符串里面包含 @wyk
思路
主要是需要注意一下整行读取字符串 getline(cin , s) 用这个需要记得 getchar() 一下,具体原因是因为函数以换行键标记读取完字符串,不然那个读入n之后的换行读进getline里面会出问题的,其他的也就是对于每个位置判断一下以当前位置开头是不是 @wyk 就可以了,这个应该不难,然后就是注意越界问题,对于当前位置需要保证后面三位都在字符串范围内,不然也会出问题。
代码
#include <bits/stdc++.h>
using namespace std;
int n;
string s, t;
bool check(int pos) {
return s[pos] == '@' && s[pos+1] == 'w' && s[pos+2] == 'y' && s[pos+3] == 'k';
}
int main() {
int ans = 0;
scanf("%d", &n); getchar();
while (n--) {
getline(cin, s);
for(int i = 0; i < s.length()-3; i++) { // 防止越界
if(check(i)) {
ans++;
break;
}
}
s = "";
}
printf("%d\n", ans);
}
G-最强对手矩阵
题意
给一个 n ∗ m n * m n∗m 的矩阵,求一个子矩阵,使得子矩阵中的所有数之和最大
思路
考虑暴力求:我们定义一个求一个矩阵和需要知道什么东西。上下左右边界对吧,所以就可以得到一种四层循环枚举边界的做法
for(int ly = 1; ly <= n; ly++) { // 上边界
for(int ry = ly; ry <= n; ry++) { // 下边界
for(int lx = 1; lx <= m; lx++) { // 左边界
for(int rx = lx; rx <= m; rx++) { // 右边界
int now = 0;
// 对于当前矩阵求和
for(int i = ly; i <= ry; i++) {
for(int j = lx; j <= rx; j++) {
now += a[i][j];
}
}
// 维护最大值
ans = max(ans, now);
}
}
}
}
这样简单粗暴的做法就出来了,这种做法看着就挺难受的(但是这种做法都想不到应该更难受吧
那么怎样才可以做些优化呢。想一下能不能每次在枚举到当前行的时候我就可以很快得求出这一行上面某一行加到这一行的和呢,具体一点我枚举到 a [ 5 ] [ 2 ] a[5][2] a[5][2] 这个数这里,我能不能很快求出 a [ 2 ] [ 2 ] + a [ 3 ] [ 2 ] + a [ 4 ] [ 2 ] + a [ 5 ] [ 2 ] a[2][2] + a[3][2] + a[4][2] + a[5][2] a[2][2]+a[3][2]+a[4][2]+a[5][2] 的值呢,这是不是一个对于 [ 1 , n ] [1,n] [1,n] 这个区间里面求任意子区间和的问题。对,前缀和,刚才面壁的同学可以应该可以知道这个东西了。那么在这里用前缀和有什么用呢? 在我们对子矩阵求和的时候本来是 O ( n ∗ m ) O(n*m) O(n∗m) 来枚举进行的,但是我们在纵向上已经知道了任意位置到当前位置的和,我现在只需要枚举横向的值就可以知道子矩阵的和了;举个例子对于 横向[1, 3] 纵向 [1, 5] 这一个矩阵求和,正常情况下是不是需要进行15次运算,然而我们现在求出纵向的前缀和 我们只需要枚举 p r e [ 5 ] [ j ] − p r e [ 1 − 1 ] [ j ] pre[5][j]-pre[1-1][j] pre[5][j]−pre[1−1][j] 这样的式子5次就可以求出和了,相当于优化了一维m。
然后我们对于枚举子矩阵也会有一些技巧。先说一下当前的策略,二维枚举一下上下界,第三维枚举有边界,这时候我们可以优化掉枚举左边界,怎么做到的呢?想象一下我们枚举到了第j列,前面的矩阵和已经求好了,我们求当前这一列的和是否要加上之前j-1列的和?是不是取决于前j-1列的和能否给当前列带来正收益,也就是说前面是正数的时候我才会去把前面的加起来,不然负数我加它干什么,我需要最大化每一个矩阵呀。这样又优化掉一维。
矩阵求和在枚举矩阵的时候可以视为 O ( 1 ) O(1) O(1) 求出来了,所以是 O ( N 2 ∗ M ) O(N^2*M) O(N2∗M) 的时间复杂度可以拿到大部分分了。
还有一种特殊情况那就是 N 比 M 大的时候就会发生 这个 O ( N 2 ) O(N^2) O(N2) 很拖累人,最大2e5的乘积完全可以造出数据让这个复杂度爆炸,那么我们就需要在 N > M的时候把矩阵转置一下,让M来承担这个比较大的数就妥妥的。
还有这个题目需要用到动态数组这个工具,看不懂的可以就视为普通的 a、b数组就可以,因为n、m的大小关系在输入之前还不明确,只能在之后定义数组大小。
代码
#include <bits/stdc++.h>
using namespace std;
int n, m;
vector <int> a[200005], b[200005];
void cg() {
if(n > m) { // 矩阵转置
for(int i = 0;i <= m; i++) {
b[i].resize(n+7);
}
for(int i = 1;i <= n; i++) {
for(int j = 1;j <= m; j++) {
b[j][i] = a[i][j];
}
}
swap(n,m);
}
else {
for(int i = 0;i <= n; i++) {
b[i].resize(m+7);
}
for(int i = 1;i <= n; i++){
for(int j = 1;j <= m; j++) {
b[i][j] = a[i][j];
}
}
}
for(int i = 1; i <= n; i++) { // 对于行求前缀和
for(int j = 1; j <= m; j++) {
b[i][j] += b[i-1][j];
}
}
}
int main() {
scanf("%d %d", &n, &m);
for(int i = 0;i <= n; i++) {
a[i].resize(m+7); // 动态数组分配固定空间
}
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
scanf("%d", &a[i][j]);
}
}
cg(); // 判断是否需要进行转置,然后把数据放到b数组上面操作
int ans = -1e9;
for(int i = 1; i <= n; i++) {
for(int j = i; j <= n; j++) { // 枚举行
int now = 0;
for(int k = 1; k <= m; k++) { // 枚举列
now = max(now, 0) + b[j][k] - b[i-1][k];
ans = max(ans, now);
}
}
}
printf("%d\n", ans);
}
H-友谊纽带
题意
问任意两个城市之间是否可以互相到达,如果可以那么计算任意两个城市之间的最远距离,否则输出-1
思路
可以先看一下 I 题的思路,如果看完了或者会并查集的话:
首先我们判断这些城市可以不可以互相通达,那么就是判断这些点是否在一个联通块里面。用并查集维护一下就可以了,如果所有的点的父亲都是同一个点,那么证明这个图就是一个联通块。就可以进行下一个操作。
因为边权是1,所以我们可以优先选择使用BFS对于每一个点作为起点,向周围扩散,一直维护最大的答案就可以了。邻接表存图各位应该不陌生了吧!(不会吧不会吧
代码
#include <bits/stdc++.h>
using namespace std;
int n, m;
vector <int> g[2003];
int fa[2003];
int fin(int x) {
if(x == fa[x]) return x;
return fa[x] = fin(fa[x]);
}
bool nice() {
int f = fin(1);
for(int i = 2; i <= n; i++) {
int h = fin(i);
if(f != h) return 0;
}
return 1;
}
int bfs(int s) {
queue <pair <int, int> > q;
bool vis[2003]; memset(vis, 0, sizeof(vis));
q.push({s, 0});
int ma = 0;
while(!q.empty()) {
auto t = q.front(); q.pop();
int p = t.first, w = t.second;
int son = 0;
for(int i : g[p]) {
if(vis[i]) continue;
vis[i] = 1; son++;
q.push({i, w+1});
}
if(!son) ma = max(ma, w);
}
return ma;
}
int main() {
scanf("%d %d", &n, &m);
for(int i = 1; i <= n; i++) fa[i] = i;
for(int i = 1; i <= m; i++) {
int u, v; scanf("%d %d", &u, &v);
int fu = fin(u), fv = fin(v);
if(fu != fv) fa[fu] = fv;
g[u].push_back(v);
g[v].push_back(u);
}
if(!nice()) return puts("-1"), 0;
int ans = 0;
for(int i = 1; i <= n; i++) {
ans = max(ans, bfs(i));
}
printf("%d\n", ans);
}
I-传送门
题意
n个城市,有m对城市之间需要修建传送门,不同的城市之间传送门的修建耗时是不一样的,求把所有城市直接或者间接联通需要的最少时间。
思路
先不说别的,首先思路肯定是用一个结构体把信息存起来,因为这些信息都是捆绑在一起的,单独拿出来都是没有意义的。然后要做的就是把这些城市用尽可能少的耗时联通起来,这里或许就想到了,我们可以把耗时作为第一关键字进行排序,使用耗时少的工程将图连接起来。
然后就是连接操作,我们可以设定每个联通块都有一个管理员,我们暂且称之为这个块的 “父亲” ,这个块的所有点都有一个父亲信息,存储的都是它。所以就可以证明他们是一个块的,这样当所有的点的父亲都是一个点的时候是不是就说明他们是一个联通块呀。
一开始所有的点都是分散的,所以首先我们先把所有的点的父亲都设置为自己,然后就开始把刚才排好序的那一堆关系拿过来准备子孙满堂了。
设 u、v分别为关系中的两个点,w是耗时,轮到这对关系加入时,我们先判断 u、v是不是本来就是在同一块了,如果本来父亲就相同,那么这个关系加的就毫无意义嘛,因为排在它前面的w一定是比现在这个w要小的呀对吧。不一样的时候,我们随便把一边的父亲设置为对方。答案加上w
那怎么检测这个点的父亲到底是谁呢?定义 f a [ x ] fa[x] fa[x] 表示 x的父亲 我们需要写一个函数搞一下,x 的父亲如果不是自己的话,那么就需要一直向上找父亲的父亲,父亲的父亲的父亲… 这样直到找到了父亲是自己的节点,那么处于递归进程的所有点的父亲都是这个点,我们在返回值的时候顺便赋值就好了,这里有点难理解,需要花时间好好想一下。这里用了一个叫做路径压缩的思想来保证了复杂度,不然形成一条链的关系那每次找父亲的时间都是 O ( n ) O(n) O(n) 的,这样绝对复杂度爆炸的。
这样一直合并一直更新父亲节点,最后维护出来的答案就一定是最小的啦,这个算法还有个名字叫做 克鲁斯卡 kruskal 用于解决这种最小生成树问题, 这里面“父亲”这种联通块的处理叫做并查集。还有一种 Prime 普里姆 同样可以解决类似的稠密图问题,就不展开讲了。
代码
#include <bits/stdc++.h>
using namespace std;
int n, m;
int fa[5003];
int fin(int x) {
if(fa[x] == x) return x;
return fa[x] = fin(fa[x]);
}
struct node {
int u, v, w;
} p[100005];
bool cmp(node a, node b) {
return a.w < b.w;
}
int main() {
scanf("%d %d", &n, &m);
for(int i = 1; i <= n; i++) fa[i] = i;
for(int i = 1; i <= m; i++) {
int u, v, w; scanf("%d %d %d", &u, &v, &w);
p[i] = {u, v, w};
}
sort(p+1, p+1+m, cmp);
int ans = 0;
for(int i = 1; i <= m; i++) {
int fu = fin(p[i].u);
int fv = fin(p[i].v);
if(fu == fv) continue;
fa[fu] = fv;
ans = max(ans, p[i].w);
}
int f = fin(1);
for(int i = 2; i <= n; i++) {
int r = fin(i);
if(r != f) {
ans = -1;
break;
}
}
printf("%d\n", ans);
}
J-井字棋残局
题意
顺序给出两个角色每次下井字棋的坐标,两个人都绝对聪明,求最后谁可以赢或者平局
思路
情况很少,只有9个格子,我们仍然可以尝试暴搜。dfs两个参数表示两个人下棋的状态,具体可以表示为二进制中的9位,第一位为1时表示棋盘 ( 1 , 1 ) (1,1) (1,1) 这个位置下了,0表示没有下,对于a,b两个数9位中都为0的位置才可以有新的棋落下,枚举终点就是当 a,b的9位都为1的时候无处可下时 或者 某一方有连续的三颗棋子了就返回。
如何判断三颗棋子连续了呢?
对于二进制中 ( 1 , 1 ) (1,1) (1,1) 表示第一位; ( 1 , 2 ) (1,2) (1,2) 表示第二位; ( 2 , 2 ) (2,2) (2,2) 表示第5位; ( 3 , 3 ) (3,3) (3,3) 表示第九位等等 所以当 第 1 , 2 , 3 1,2,3 1,2,3 位按位取与为1时证明三颗连续,同理的还有 (4,5,6) (7,8,9) (1,4,7) (2,5,8) (3,6,9) (1,5,9) (3,5,7) 共八种情况
我们定义 2 为 赢,1为平局,0为输
当在a的回合,dfs的结果为0 表示下一回合是b的时候输了,证明a赢了,所以存在a可以赢的情况,由于a绝对聪明所以a一定可以赢,这就是a的必胜态,这个时候需要一个变量来记录a可以为2
同样在a的回合,dfs的结果为2,表示下回合b可以赢,这个时候就需要一个变量来记录a在所有走棋的情况下是否都会输,也就是都为0,如果没有除了b返回2的别的情况,那么a不管下在什么位置都会输,在a怎么聪明都无济于事,这就是a的必输态
如果返回的是1,那么就说明a下的当前位置,导致了可以与b平局,那么变量也可以记录下来。
在所有情况都讨论完的时候看这个变量怎么个情况,如果有2的话那么说明a是可以赢的,那么在这一步a一定会选择赢,否则就看是否有1,不能赢最起码追求一下平局,否则就会输。
经过这个讨论之后其实这个变量总共就用一个数就可以了,不断在每一次走棋的时候取max,看最后是多少就代表最好的情况是什么。最后就会将所有情况返回到最初,就可以预测比赛的结果了。
代码
#include <bits/stdc++.h>
using namespace std;
bool win(int k) {
return
(((k>>1)&1) & ((k>>2)&1) & ((k>>3)&1)) ||
(((k>>4)&1) & ((k>>5)&1) & ((k>>6)&1)) ||
(((k>>7)&1) & ((k>>8)&1) & ((k>>9)&1)) ||
(((k>>1)&1) & ((k>>4)&1) & ((k>>7)&1)) ||
(((k>>2)&1) & ((k>>5)&1) & ((k>>8)&1)) ||
(((k>>3)&1) & ((k>>6)&1) & ((k>>9)&1)) ||
(((k>>1)&1) & ((k>>5)&1) & ((k>>9)&1)) ||
(((k>>3)&1) & ((k>>5)&1) & ((k>>7)&1));
}
int dp[1<<10][1<<10];
int dfs(int a, int b) {
if(dp[a][b] != -1) return dp[a][b];
if(win(a)) return 0;
if(a + b == (1<<10)-2) return 1;
int res = 0;
for(int i = 1; i <= 9; i++) {
if((a>>i)&1 || (b>>i)&1) continue;
int t = dfs(b+(1<<i), a);
if(t == 0) res = max(res, 2);
if(t == 1) res = max(res, 1);
if(t == 2) res = max(res, 0);
}
return dp[a][b] = res;
}
void out(int k) {
int num = 10;
while(num--) {
printf("%d", k&1);
k >>= 1;
}
puts("");
}
int main() {
memset(dp, -1, sizeof(dp));
int _; scanf("%d", &_); while(_--) {
int X = 0, O = 0;
int n; scanf("%d", &n); for(int i = 1; i <= n; i++) {
int x, y; scanf("%d %d", &x, &y);
if(i % 2) X += 1<<((x-1)*3+y);
else O += 1<<((x-1)*3+y);
}
// out(X); out(O);
if(n%2) {
int res = dfs(X, O);
if(res == 0) puts("X");
if(res == 1) puts("-1");
if(res == 2) puts("O");
}
else {
int res = dfs(O, X);
if(res == 0) puts("O");
if(res == 1) puts("-1");
if(res == 2) puts("X");
}
}
}