前言
这是我在学校留校的第一个暑假,时间戳为大一结束的暑假。为时两周,进行算法集训(摸鱼)后续会不断进行补题(也许吧)
符号说明:标明 * 的内容表示为知识补充,标明 TODO 的内容表示还未完成
day01
比赛传送门
T1【模拟】
原题链接
大致题意是:打印某种有规律的图形
我的思路
-
观察样例,按照要求进行模拟即可
-
时间复杂度 O ( n ) O(n) O(n)
AC代码文章来源:https://www.toymoban.com/news/detail-540449.html
void solve()
{
int n;
cin >> n;
for (int i = 0; i < 3 * n; i++)
{
for (int j = 0; j < n; j++) cout << "*";
for (int j = 0; j < 2 * n; j++) cout << ".";
for (int j = 0; j < n; j++) cout << "*";
cout << "\n";
}
for (int i = 1; i <= n; i++)
{
for (int j = 0; j < i; j ++) cout << ".";
for (int j = 0; j < n; j ++) cout << "*";
for (int j = 0; j < 2 * n - 2 * i; j ++) cout << ".";
for (int j = 0; j < n; j ++) cout << "*";
for (int j = 0; j < i; j ++) cout << ".";
cout << "\n";
}
}
T2【模拟】
原题链接
大致题意是:给定一个队列,如果是猫就喂食,如果是狗,也喂食,但是猫粮会增加一定的数量,问能否让队列中的所有狗都吃到狗粮
我的思路
-
遍历字符串:若是猫,检查是否还有猫粮,如果没有,结束遍历;如果还有,猫粮-1。若是狗,检查是否还有狗粮,如果没有,结束遍历;如果还有,狗粮-1,同时猫粮+m。
最后从上述遍历结束的指针继续往后检查是否还存在没有吃到狗粮的狗即可。
-
时间复杂度 O ( n ) O(n) O(n)
AC代码
#include <iostream>
using namespace std;
void solve(int num)
{
cout << "Case #" << num << ": ";
int n, d, c, m;
cin >> n >> d >> c >> m;
string s;
cin >> s;
int i;
for (i = 0; i < n; i++)
if (s[i] == 'C')
{
if (!c) break;
--c;
}
else
{
if (!d) break;
--d;
c += m;
}
bool ok = true;
for (int j = i; j < n; j++)
if (s[j] == 'D')
{
ok = false;
break;
}
if (ok) cout << "YES" << "\n";
else cout << "NO" << "\n";
}
int main()
{
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
int T; T = 1;
cin >> T;
// while(T--) solve();
for (int i = 1; i <= T; i ++) solve(i);
return 0;
}
T3【模拟】
原题链接
大致题意是:给定三个数可执行一种操作,即“选择一个数,将他以另一个比他大的数为中点进行跳跃获得一个新数”,则经过多少次跳跃,可以获得最大的一个数使其超过某个给定的数
我的思路
-
按照要求进行迭代计算即可。按照贪心的策略,每次将最左边的一个数以最右边的一个数为中点进行跳跃,然后判断最右边的数是否达到了n即可
-
时间复杂度 O ( l o g n ) O(log\ n) O(log n):精准计算过程个人不太会,于是我就按照最笨的方法计算最高的时间复杂度。即,假设 a = 2 , b = 3 , c = 4 a=2,b=3,c=4 a=2,b=3,c=4,进行迭代,迭代后的数组为:
2 3 4 6 9 14 22 35 56
不难发现,这个数列的后一项与前一项的差构成了一个斐波那契数列。我们可以近似的将这一串数字看做斐波那契额数列的前缀和。由于斐波那契额数列的增长是指数级别的,那么其前缀和自然也是指数级别的。因此想要让前缀和达到n的时间复杂度就是对数级别的,即 O ( l o g n ) O(log\ n) O(log n),证毕。
AC代码
void solve()
{
int l, m, r, n;
cin >> l >> m >> r >> n;
int res = 0;
while (r < n)
{
res++;
int t = l;
l = m;
m = r;
r = 2 * r - t;
}
cout << res << "\n";
}
T4【哈希、组合数学】
原题链接
大致题意是:给了一个数组,每一个数字被赋予了红或蓝的颜色,问这个数组中有多少个数字相同,颜色不同的数字对。
我的思路
-
一开始WA了两发;
一发是因为纯暴力,想看过了几个测试点,结果就过了一个hhh;暴力很好写,就是枚举数字,然后查找相同数字,如果颜色不同计数器+1即可,不再赘述。
还有一发是思考角度有问题,我从数字出发,想要统计每种数字中颜色的分布关系。但其实很难实现,也就没能成功。
-
最后AC思路是,统计两种颜色中数字的组成情况,与上述第二个思路相反。即:每个颜色统计数字的分布情况,最后对照两个颜色数组中数字的组成情况,按照组合数学的计算公式计算即可。
-
时间复杂度: O ( n log n ) O(n\log{n}) O(nlogn)
AC代码
void solve()
{
int n; cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i ++) cin >> a[i];
string s; cin >> s;
vector<int> r, b;
for (int i = 0; i < n; i++)
if (s[i] == 'R') r.emplace_back(a[i]);
else b.emplace_back(a[i]);
sort(r.begin(), r.end());
sort(b.begin(), b.end());
ll res = 0;
for (int i = 0, j = 0; i < r.size() && j < b.size(); )
if (r[i] < b[j]) i++;
else if (r[i] > b[j]) j++;
else
{
int flag = r[i];
int x = 0, y = 0;
while (r[i] == flag) x++, i++;
while (b[j] == flag) y++, j++;
res += x * y;
}
cout << res << "\n";
}
T5【贪心、双指针】
原题链接
大致题意是:给定字符串s,问经过最少多少步,可以将其转化为0、1数量不变,但是是01相间的字符串
我的思路
-
这道题比赛的时候没做出来,qaq。也想到了和原串对比,也想到了下标作差,但是唯独没想到正确的贪心思路。
-
即最小交换次数就是将每一个0或1按照排列的顺序依次从左往右移到模板串中正确的位置上即可。
-
代码实现使用双指针。一个指针是在模板串上,寻找0或1的位置;一个指针是在给定的串上寻找模板串开头的数字的下标,比如如果给的串0比1多,则在给定串上寻找每一个0的位置进行计算累加即可,而如果1比0多,则在给定串上寻找每一个1的位置进行计算累加即可。而对于0和1的数量相同的情况,取上述两种计算结果的最小值即可。
-
需要注意的是这道题要开long long,原题测试是需要开的,计算最大结果的方式如下:假设是最坏的情况,即给点串全部都是左边的一般半0和右边的一半1组成,那么第一个1移第一个位置需要 n 2 \frac{n}{2} 2n,第二个1移到第二个位置需要 n 2 − 1 \frac{n}{2}-1 2n−1,以此类推,最后一个1移到最后一个位置需要1,共有 n 2 \frac{n}{2} 2n次移动,那么这 n 2 \frac{n}{2} 2n次移动步数之和就是 n 2 n^2 n2级别的,因此需要开long long
-
时间复杂度 O ( n ) O(n) O(n)
AC代码
typedef long long ll;
ll calc(string& s, char c)
{
// i是标准01串的下标,j是给定串的下标
ll cnt = 0;
for (int i = 0, j = 0; i < s.size(); i += 2, j++)
{
while (s[j] != c) ++j;
cnt += abs(i - j);
}
return cnt;
}
void solve()
{
string s; cin >> s;
int cnt0 = count(s.begin(), s.end(), '0');
int cnt1 = count(s.begin(), s.end(), '1');
ll res = 0;
if (cnt1 > cnt0)
res = calc(s, '1');
else if (cnt1 < cnt0)
res = calc(s, '0');
else
res = min(calc(s, '1'), calc(s, '0'));
cout << res << "\n";
}
T6【01背包】TODO
原题链接
大致题意是:给定一个只由 0 − 9 0-9 0−9 组成的字符串,在其中选出一些字符,使得这些数组成的新数是 9 的倍数,问共有多少种选法
我的思路
-
这题比赛的时候都没怎么看,因为来不及看了qwq,显然是动态规划问题。然后此题是动规中01背包问题的变式。
-
当然,对于01背包问题,可以使用指数级别的枚举,时间复杂度是 O ( 2 n ) O(2^n) O(2n)
AC代码
题解文件 - zh
day02
讲题、补题,无比赛
T1【最长公共子序列】*
这是曾经做过的一道题,但是今天上台讲解的时候连题目都说不明白😢,特此补一下题解加深记忆。
原题链接
大致题意:给定 s s s 串与 t t t 串,可以对 t t t 串进行以下操作:选中 t t t 串的一个字符,将其移动到 t t t 串的其他位置。问:最少可以进行多少次操作可以将 t t t 串转化为 s s s 串,如果不可以输出 − 1 -1 −1
我的思路
-
首先需要注意的是t串一定要包含s串的所有字符及其相应的个数,只能多不能少。
其次就是算法设计的问题了:假设s串的固定长度为len,那么t串的最少移动次数就是
len-s与t已经匹配的最长公共子序列
。于是这道题的核心思路就是计算s与t串的最长公共子序列。
-
时间复杂度 O ( n m ) O(nm) O(nm),其中n为给定串的长度,即18;m为每一个测试点给的长度,总长不超过 5 × 1 0 5 5\times10^5 5×105,大约是 1 0 7 10^7 107级别,完全OK。
AC代码
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
bool ok(string& s, string& t)
{
for (int i = 0; i < s.size(); i++)
if (count(t.begin(), t.end(), s[i]) < count(s.begin(), s.end(), s[i]))
return false;
return true;
}
void solve()
{
string s = "crazythursdayvme50";
string t;
cin >> t;
if (!ok(s, t))
{
cout << -1 << "\n";
return;
}
// 便于使用最长公共子序列的模板
s = '0' + s;
t = '0' + t;
// f[i][j]表示s串的[1->i]个字符与t串的前[1->j]个字符的最长公共子序列
vector<vector<int>> f(20, vector<int>(t.size() + 10));
for (int i = 1; i <= 18; i++)
for (int j = 1; j <= t.size(); j++)
if (s[i] == t[j])
f[i][j] = f[i - 1][j - 1] + 1;
else
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
cout << 18 - f[18][t.size() - 1] << "\n";
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T; cin >> T;
while (T--) solve();
return 0;
}
day03
比赛传送门
T1【模拟】
原题链接
题意大致是:给两个长度相同的字符串,问其中不同的字符数
我的思路
- 扫一遍计数即可
AC代码
void solve()
{
string s = "codeforces";
string t; cin >> t;
int res = 0;
for (int i = 0; i < s.size(); i++)
if (s[i] != t[i])
res ++;
cout << res << "\n";
}
T2【模拟】
原题链接
题意大致是:给定两个字符串s和t,其中只有三种字符,其中两种字符是等价的,问这两个字符是否相等(等价字符算作相同字符)
我的思路
- 将两种等价的字符转化为同一个字符,最后使用string的比较运算符即可
AC代码
void solve()
{
int n; cin >> n;
string s, t;
cin >> s >> t;
for (int i = 0; i < n; i++)
if (s[i] == 'G')
s[i] = 'B';
for (int i = 0; i < n; i++)
if (t[i] == 'G')
t[i] = 'B';
if (s == t) cout << "YES" << "\n";
else cout << "NO" << "\n";
}
T3【模拟】
原题链接
题意大致是:给定字符串s,将其中的每个字符s[i]做一下变化:
- 在当前字符s[i]后面添加一个字符串,也可能不添加
- 再添加一个s[i]在后面
给了这个变化后的字符串,让你找到原始的那个字符串
我的思路
- 其实就是找到相同的两个字符,然后保留一个添加到答案串即可
AC代码
void solve()
{
int n; cin >> n;
string s; cin >> s;
char ch = s[0];
string res;
res += ch;
for (int i = 1; i < n;)
{
// 寻找到与ch相同的字符
while (s[i] != ch) i++;
// 再往后一位重新寻找ch
i++;
// 在没有越界的情况下
if (i < n)
{
// 添加当前字符到答案串
res += s[i];
// 重新定义ch
ch = s[i];
// 指针右移寻找后面与ch相同的字符
i++;
}
}
cout << res << "\n";
}
T4【数学】
原题链接
大致题意为:在二维方格图中给定了三个点,问从其中一个点到另外两个点在走最短路(其实就是曼哈顿距离)的情况下,最多能一起走多远
我的思路
-
思路其实非常简单,由于x轴与y轴是相互独立的,因此我们可以将二维图形转化为一维,即分别判断x轴与y轴的三个点的相对情况(n维也是同理)
如果A点在B、C一侧,即与B or C的最近距离
如果在B、C之间,则为0
AC代码
void solve()
{
int xa, ya; cin >> xa >> ya;
int xb, yb; cin >> xb >> yb;
int xc, yc; cin >> xc >> yc;
int res = 1;
if ((xa <= xb && xa <= xc) || (xa >= xb && xa >= xc))
res += min(abs(xa - xb), abs(xa - xc));
if ((ya <= yb && ya <= yc) || (ya >= yb && ya >= yc))
res += min(abs(ya - yb), abs(ya - yc));
cout << res << "\n";
}
T5【bfs | dfs】
原题链接
-
大致题意是:给定一个数num和一个目标值target,要求对num进行以下操作
- 如果num可以被3整除,那么就将num转化为 2 × n u m 3 2 \times \frac{num}{3} 2×3num和 n u m 3 \frac{num}{3} 3num
可以进行任意次上述操作,问能否得到target
我的思路
问的是能否找到即可,那么其实宽搜和深搜都是可以实现的
-
方法一:宽搜
逻辑很简单,对于每一个数,如果等于target,那么直接跳出循环即可,如果可以被3整除,那么就拆成两个数然后放入队列
-
AC代码
void solve() { int num, tar; cin >> num >> tar; queue<int> q; q.push(num); bool ok = false; while (!q.empty()) { int t = q.front(); q.pop(); if (t == tar) { ok = true; break; } if (t % 3 == 0) { q.push(t / 3); q.push(2 * t / 3); } } cout << (ok ? "Yes" : "No") << "\n"; }
-
方法二:深搜
代码很简单,逻辑的话其实就是画出一棵二叉搜索树,然后加上一些限制条件进行搜索就可以了。
非常适合我这种搜索小🐮🐴进行练习与理解 -
AC代码
void dfs(int num, int tar, bool& ok) { if (num == tar) { ok = true; return; } if (num % 3 == 0) { dfs(num / 3, tar, ok); dfs(2 * num / 3, tar, ok); } } void solve() { int num, tar; cin >> num >> tar; bool ok = false; dfs(num, tar, ok); cout << (ok ? "Yes" : "No") << "\n"; }
- 这道题在cf里面分数评定才为1000(还是我太菜了)。
- 在用两种方法写完了这道题后,我想出了一个改编的思路:
- 比如加强数据使得只能用bfs,时间复杂度的计算也很简单,对于bfs的时间复杂度就是结点数;
- 强化数据后可能会涉及long long;
- 当然问题也要随之改变,即求num到target的最短距离(但是发现貌似num到target的长度的都是相等的),于是我就想着换一下拆分数的规则,比如如果是4的倍数就拆成1 : 3的数据,对于3和4的公倍数,就还是按照3的倍数进行拆分,那么最终答案就是
dist[num][target]
- 可能会在下一次出题的时候完善出来吧hhh。感觉对于dfs这种深搜问题,先画出搜索树,再进行编码比较容易理解与掌握
T6【dp】TODO
原题链接
T7【数学】TODO
原题链接
T8【list】*
主要是介绍一下C++标准库中 list
的 reverse_iterator
迭代器用法,即++操作就是让迭代器从右往左移动
void solve()
{
list<int> L;
int a[] = {3, 1, 5, 1, 8, 4}, n = 6;
for (int i = 0; i < n; i++)
L.push_back(a[i]);
list<int>::reverse_iterator it = L.rbegin();
for (int i = 0; i < n; i++)
cout << *(it++) << ' ';
}
输出为
4 8 1 5 1 3
T9【二进制枚举 | dfs+剪枝】*
大致题意就是从n个数中选k个数使得这k个数之和为质数,问共有多少种选法
方法有两种
-
一种是二进制枚举,时间复杂度是满的 O ( n 2 n max a [ i ] ) O(n\ 2^n\sqrt {\max a[i]}) O(n 2nmaxa[i]),如下
#include <iostream> #include <vector> using namespace std; // 判断质数的函数 bool is_prime(int x) { if (x <= 1) return false; for (int i = 2; i <= x / i; i++) if (x % i == 0) return false; return true; } int main() { // 读取数据 int n, k; cin >> n >> k; // 读取数据 vector<int> a(n); for (int i = 0; i < n; i++) cin >> a[i]; // 定义答案 int res = 0; // 二进制枚举 for (int i = 0; i < (1 << n); i++) { // 统计是否选择了k个数 int cnt = 0; vector<int> v(25); for (int j = 0; j < n; j++) if (i & (1 << j)) v[cnt++] = a[j]; // 如果不是k个数就继续枚举 if (cnt != k) continue; // 如果是k个数就计算和是否为质数 int sum = 0; for (int j = 0; j < k; j++) sum += v[j]; if (is_prime(sum)) res++; } // 输出答案 cout << res << "\n"; return 0; }
-
另一种思路是搜索,加剪枝,虽然也是指数级别的算法,但是加上剪枝就会比上面的满 O ( 2 n ) O(2^n) O(2n)好,如下
#include <iostream> #include <vector> using namespace std; // 判断质数的函数 bool is_prime(int x) { if (x <= 1) return false; for (int i = 2; i <= x / i; i++) if (x % i == 0) return false; return true; } // 返回:选了m个数,起始枚举的数组下标为idx的情况下,这m个数之和sum为质数的方案数 int dfs(int m, int sum, int idx, int n, int k, vector<int>& a) { int res = 0; if (m == k && is_prime(sum)) { res++; return 1; } // 剪枝 if (m + n - idx < k) return 0; for (int i = idx; i < n; i++) res += dfs(m + 1, sum + a[i], i + 1, n, k, a); return res; } int main() { // 读取数据 int n, k; cin >> n >> k; // 读取数据 vector<int> a(n); for (int i = 0; i < n; i++) cin >> a[i]; cout << dfs(0, 0, 0, n, k, a) << "\n"; return 0; }
day04 & day05
比赛传送门
T1【模拟】
原题链接
大致题意是:判断度数列是否可图画
我的思路
- 根据每条边可以连接两个点可知一个度数列如果可图画,那么度数列之和一定是偶数
- 时间复杂度 O ( n ) \mathcal O(n) O(n)
AC代码
void solve()
{
int n; cin >> n;
ll sum = 0;
for (int i = 0; i < n; i ++)
{
ll x; cin >> x;
sum += x;
}
cout << ((sum % 2) ? "No" : "Yes") << "\n";
}
T2【并查集】
原题链接
大致题意是:告诉你那些药水可以两两进行反应,问如何将药水放置到试剂瓶中可以使得反应次数最多
我的思路
- 我们可以根据反应关系构建多叉树(如果重复反应,可以通过路径压缩处理掉),然后由贪心思想,从根开始放置药水即可获得最多反应次数
- 需要注意的一点是,构建出来的多叉树可能不止一棵,因此最终的反应次数就是所有多叉树的结点数减去树的数量
- 时间复杂度 O ( n ) \mathcal O(n) O(n)
AC代码
int n, m;
int fa[55], sum[55];
ll qmi(ll a, ll b)
{
ll res = 1;
while (b)
{
if (b & 1) res = res * a;
a = a * a;
b >>= 1;
}
return res;
}
int find(int x)
{
if(fa[x] != x) fa[x] = find(fa[x]);
return fa[x];
}
void merge(int x, int y)
{
int fx = find(x), fy = find(y);
if(fx != fy)
{
sum[fy] += sum[fx];
fa[fx] = fy;
}
}
void solve()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
fa[i] = i;
sum[i] = 1;
}
while (m--)
{
int x, y;
cin >> x >> y;
merge(x, y);
}
unordered_map<int, bool> ha;
for (int i = 1; i <= n; i++)
{
int father = find(i);
ha[father] = true;
}
int cnt = 0, hi = 0;
for (auto x : ha)
cnt++, hi += sum[x.first];
hi -= cnt;
cout << qmi(2, hi) << "\n";
}
T3【分治】TODO
原题链接
大致题意是:求一个数列的逆序对的数量
我的思路
- 这道题很明显的就是利用归并排序计算出一个数列中逆序对的数量,但其实我对这个
merge_sort
函数的内容并不能完全理解,特此留一个坑 - 时间复杂度 O ( n log n ) \mathcal O(n \log{n}) O(nlogn)
AC代码
const int N = 1e5 + 10;
int n;
vector<ll> q(N), tmp(N);
ll res;
ll merge_sort(int l, int r)
{
if (l >= r) return 0;
ll mid = (l + r) >> 1;
merge_sort(l, mid), merge_sort(mid + 1, r);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r)
if (q[i] <= q[j])
tmp[k++] = q[i++];
else
{
tmp[k++] = q[j++];
res += mid - i + 1;
}
while (i <= mid) tmp[k++] = q[i++];
while (j <= r) tmp[k++] = q[j++];
for (i = l, j = 0; i <= r; i++, j++)
q[i] = tmp[j];
return res;
}
void solve()
{
cin >> n;
for (int i = 0; i < n; i++)
cin >> q[i];
res = merge_sort(0, n - 1);
if (res < 6)
{
cout << "YES" << "\n";
return;
}
else
cout << "NO" << ' ' << res << "\n";
}
T4【背包】TODO
原题链接
T5【数论】
原题链接
大致题意是:有1~n共n个数,问:如何从1~n选k个数,使得这k个数的最大公约数的尽可能的最大(对于一个人,最大公约数就是这个人本身的数)
我的思路
- 这道题说不上来的感觉,我觉得答案非常的明显,就是 ⌊ n k ⌋ \left \lfloor \frac{n}{k} \right \rfloor \quad ⌊kn⌋
- 时间复杂度 O ( 1 ) \mathcal O(1) O(1)
AC代码
void solve()
{
int n, k;
cin >> n >> k;
cout << (n - n % k) / k << "\n";
}
T6【dp】TODO
原题链接
T7【二分】
原题链接
大致题意是:给定路径总长,路上原有的障碍物的数量和位置,给你n次放置障碍物的操作,使得最终路径上的任意两个障碍物之间的最大距离最小,求出最小的最大距离
我的思路
其实这道题我用优先队列维护任意两个障碍物之间的距离,然后每次将最大的距离一分为二,最终输出这个优先队列的队头,但是答案很不幸。。。留坑。。。
改编自洛谷,改编后的代码更加符合逻辑,没有坑点,一道很好的二分答案
我们知道,一定可以找到最终的最大距离,一个额外的障碍物都不放,那么答案就是当前局面中所有间隔中的最大值。如果想要减小这个最大值,很显然就需放置额外的障碍物。可以发现放置的障碍物越多,最大距离就越小,具有单调性,可以进行二分。我们通过二分最大距离,通过判断当前最大距离需要花费的障碍物数量来检查当前的最大距离是否可行,最终使用二分左模板获得最小的最大距离即可。计算所需的障碍物数量的计算公式为
⌈
a
[
i
]
−
a
[
i
−
1
]
d
i
s
t
m
a
x
⌉
−
1
\left \lceil \frac{a[i]-a[i-1]}{dist_{max}}\right \rceil - 1
⌈distmaxa[i]−a[i−1]⌉−1
时间复杂度 O ( n log n ) O(n \log{n}) O(nlogn)
AC代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6;
int lim, n, k;
int a[N];
bool chk(int x) {
int cnt = 0;
for (int i = 1; i < n; i++) {
int gap = a[i] - a[i - 1];
if (gap > x) {
cnt += (gap + x - 1) / x - 1;
}
}
return cnt > k;
}
void solve() {
cin >> lim >> n >> k;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
int l = 1, r = lim;
while (l < r) {
int mid = (l + r) >> 1;
if (chk(mid)) l = mid + 1;
else r = mid;
}
cout << r << "\n";
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int T = 1;
// cin >> T;
while (T--) solve();
return 0;
}
day06
记录人生中的首次AK!!!
- 虽然是小小的一次为时2h的小比赛
- 虽然最高难度只是cf的Div3的C题
- 但是身为算法小🐮🐴的我还是很开心的hhh
T1【模拟】
原题链接
大致题意是:给定一个公式,计算结果
我的思路
- 按需模拟即可
- 注意C++中的浮点数保留小数的操作
- 时间复杂度 O ( 1 ) \mathcal O(1) O(1)
AC代码
void solve()
{
double M, m, v;
cin >> M >> m >> v;
cout << fixed << setprecision(2) << (m * v / (M - m)) << "\n";
}
T2【数学】
原题链接
大致题意是:给定一个数n,可以进行以下两种操作
- 选择一个大于1的正整数x,将n转化为n%x
- 选择一个大于0的正整数x,将n减去x
代价是所有的x之和,然后求让n变为0的最小代价
我的思路
-
根据给的数据范围就知道一定是找规律推公式
- 如果是偶数,直接%2就可以了
- 如果是奇数,先%2再-1即可
- 特判0和1即可
-
时间复杂度 O ( 1 ) \mathcal O(1) O(1)
AC代码
void solve()
{
ll n;
cin >> n;
int res = 0;
if (n == 0)
res = 0;
else if (n == 1)
res = 1;
else if (n % 2 == 0)
res = 2;
else
res = 3;
cout << res << "\n";
}
T3【双指针 & 组合数学】
原题链接
大致题意是:给定一个由小写字母和?组成的字符串,可以将?转化为任意小写字母,问使得字符串转化为回文串一共有多少种方案
我的思路
-
其实就是判断字符串首尾的字符情况,分奇数与偶数讨论,但其实最终落实到代码上奇数与偶数的是一样的
- 首尾相同且均不是?,那么就是这种情况,方案数不变
- 首尾相同且均是?,那么这种情况就有26种方案数,因为a~z均可
- 首尾不同,其中一个是a~z,另一个是?,那么这个?,就只能是前面的这个字符,因此方案数不变
对于奇数位只需要讨论最中间的那个字符
- 如果是小写字母,那么方案数不变
- 如果是?,则方案数有26种
最后一种特判就是如果两个都是字符但是不相同,那么就不可能构成回文串,那么答案就是0
然后统计26方案的数量hi,最后根据组合数学的思路,答案就是 2 6 h i 26^{hi} 26hi
-
时间复杂度 O ( n ) \mathcal O(n) O(n)
AC代码
void solve()
{
string s; cin >> s;
int i = 0, j = s.size() - 1;
ll hi = 0;
bool ok = true;
while (i <= j)
{
if (s[i] == '?' && s[j] == '?')
hi++;
if (s[i] != '?' && s[j] != '?' && s[i] != s[j])
{
ok = false;
break;
}
i++, j--;
}
if (ok)
cout << qmi(26, hi, MOD) << "\n";
else
cout << 0 << "\n";
}
T4【图 | 思维题】
原题链接
定义基环树为:一个n个点,n条边,没有自环和重边的无向连通图
定义一个图的直径为:任意两点最短路的最大值
那么由 n 个结点构成的基环树中的最小直径是多少
我的思路
-
看似很离谱的问题,因为数据很大,建图是不太可能的,那么就可以仔细理解直径同时画图进行实操hhh
为了让直径尽可能的小,那么我们画出的图中每个点之间的距离就一定要尽可能的近,于是我们想到画一个星射状的图,此时点数为n,边数为n-1,还差一条边就选任意两个还没有连起来的点连起来即可
最终得到的结论就是:只要是 n ≥ 4 n \ge 4 n≥4,则答案一定是2,如果 n = 3 n=3 n=3,则答案是1
-
时间复杂度 O ( 1 ) \mathcal O(1) O(1)
AC代码
void solve()
{
int n; cin >> n;
if (n == 3)
{
cout << 1 << "\n";
return;
}
cout << 2 << "\n";
}
T5【贪心 & 排序】
原题链接
大致题意是:给了n个数,定义攻击力为这个数列中后一个数与前一个数的差值的绝对值的总和。在不改变排列顺序的前提下,需要将这n个数分成k个组,这k个组的攻击力计算方式与上述一致,问如何划分组别使得最终的攻击力最小
我的思路
-
毕竟是cf的Div2的A题,是吧,那就没什么好多说的了,其实就是对于n个数,那么就一共有n-1个间隔数 n u m [ i ] num[i] num[i],而如果划分成k个组,其实就是减掉前面n-1个间隔中的k-1个间隔,而为了让这最终的攻击力最小,即求
m i n { ∑ i = 1 ( n − 1 ) − ( k − 1 ) n u m [ i ] } ,即 m i n { ∑ i = 1 n − k n u m [ i ] } min\{\sum_{i=1}^{(n-1)-(k-1)}num[i]\},即\ min\{\sum_{i=1}^{n-k}num[i]\} min{i=1∑(n−1)−(k−1)num[i]},即 min{i=1∑n−knum[i]}
于是想到将num数组进行排序,最终得到的最小攻击力就是num数组的前n-k个数之和 -
时间复杂度 O ( n log n ) \mathcal O(n\log{n}) O(nlogn)
AC代码
void solve()
{
int n, k;
cin >> n >> k;
vector<int> a(n);
for (int i = 0; i < n; i++)
cin >> a[i];
// 计算num[i]
vector<int> num;
for (int i = 1; i < n; i++)
num.emplace_back(abs(a[i] - a[i - 1]));
sort(num.begin(), num.end());
int res = 0;
for (int i = 0; i < n - k; i++)
res += num[i];
cout << res << "\n";
}
T6【贪心】
原题链接
- 题目背景还是ICPC的比赛规则
- cf的Div3的C题,就没什么好说的了吧🐶,但还是🐸了三发nnd
- 还是给你一个比赛成绩,即AC的题目数与总罚时,问你某个人最终的排名。已知总题目数m,共n个人的做m题需要的时间以及比赛的总时间h
- 唯一的区别是现在每个参赛队员都知道自己某道题做出来需要的时间,大家都是按照最优的做题顺序来做题
我的思路
- 很显然的按照罚时的从小到大的顺序来做题
排序不等式 - 然后就是纯模拟即可
AC代码
// 某人的AC数与总罚时的计算函数
pair<int, int> calc(vector<int>& a, int m, int h)
{
// cnt为AC数,t为总罚时
int cnt = 0, t = 0;
// 贪心策略,按照罚时的升序来做题
sort(a.begin(), a.end());
// 每道题的罚时是:开始做这道题时比赛已经经过的时间s[i - 1] + 完成此题需要的时间a[i],因此需要计算前缀和
// 那么s[i]就表示完成第i题后的时间戳,显然这个s[i]是升序的
vector<int> s;
s.push_back(a[0]);
for (int i = 1; i < m; i++)
s.push_back(a[i] + s[i - 1]);
// 枚举每一个时间戳
for (int i = 0; i < m; i++)
{
// 如果AC掉这道题比赛时间还没结束,那么这道题就可以被AC掉并且总罚时加上这道题的罚时s[i]
if (h >= s[i])
{
cnt++;
t += s[i];
}
else break;
}
return make_pair(cnt, t);
}
// 第一关键词是AC的题目,第二关键词是总罚时
bool cmp(pair<int, int>& a, pair<int, int>& b)
{
if (a.first == b.first) return a.second < b.second;
return a.first > b.first;
}
void solve()
{
// n个人,m道题,总比赛时间h
int n, m, h;
cin >> n >> m >> h;
// 存每个人的AC数与总罚时
vector<pair<int, int>> arr;
// 第一个人的做每道题需要的时间
vector<int> a(m);
for (int i = 0; i < m; i++)
cin >> a[i];
// 获得第一个人的AC数与总罚时,即题目要求需要找排名的这个人
pair<int, int> flag = calc(a, m, h);
arr.push_back(flag);
// 计算其余选手的AC数与总罚时
for (int x = 1; x < n; x++)
{
vector<int> v(m);
for (int i = 0; i < m; i++)
cin >> v[i];
arr.push_back(calc(v, m, h));
}
// 按照ICPC的比赛规则,利用C++中的STL进行排序
sort(arr.begin(), arr.end(), cmp);
// 目标角色的下标
int idx = 0;
// 寻找这个人的具体下标从而得到排名
for (int i = 0; i < arr.size(); i++)
if (arr[i] == flag)
{
idx = i + 1;
break;
}
// 输出下标
cout << idx << "\n";
}
day07
放假日~
day08
比赛传送门
T1【最长上升子序列】
原题链接
大致题意是:求最长公共子序列,模板题就没什么好解释的了
我的思路
抛开本道题的数据量,此题理论上有两种做法
-
法一:朴素动规
-
dp[i]
表示前 i i i 个数字中比当前数字小的数字的个数(由于求的是最长上升子序列,因此自身也算一个)动规三步骤
- 确认属性值(最长上升子序列)
- 划分答案集合(1. 前面的数比自己小 2. 前面的数比自己大)
- 归纳出状态转移方程,如果是1. 则
dp[i] = max(dp[i], dp[j] + 1)
,如果是2. 则dp[i]
保持不变
第一层枚举每个数,第二层枚举当前数之前的所有的数。就可以确保不漏答案
-
时间复杂度 O ( n 2 ) \mathcal O(n^2) O(n2)
-
AC代码
void solve() { // 读数据 int n; cin >> n; // 读数据 vector<int> a(n + 1); for (int i = 1; i <= n; i++) cin >> a[i]; // dp[i]表示前i个数字中比自己小的数字的个数 vector<int> dp(n + 1, 0); // 初始化dp数组 dp[1] = 1; for (int i = 1; i <= n; i++) { dp[i] = 1; for (int j = 1; j < i; j++) if (a[j] < a[i]) dp[i] = max(dp[i], dp[j] + 1); } int res = 0; for (int i = 1; i <= n; i++) res = max(res, dp[i]); cout << res << "\n"; }
-
法二:二分优化动规(贪心思想)
-
可以发现,我们在每次更新
dp[i]
时,需要枚举从1
到i-1
的每个dp[j]
,这其中是有很多重复的,于是思考优化我们放弃
dp
数组,采用last
数组与len变量
其中
last[len]
表示长度为len
的最长上升子序列的最后一个数字为了让一个子序列尽可能的长,就需要这个子序列的最后一个数尽可能的小【贪心】
最后得到的这个
last
数组中每个数都是严格单调递增的在利用a数组中的每个数更新
last
数组时,对于每个last[i]
,我们想要找一个比a[i]
小的且最大的那个数-
可以采用遍历整个
last
数组来寻找这个target
数 -
当然,由于
last
中的数有单调性,因此可以采用二分来加速查询-
由于每个长度对应的数都是唯一的,因此理论上两个模板都是可行的
-
模板1:查左边的那个数
int l = 1, r = len; while (l < r) { int mid_len = (l + r) >> 1; if (last[mid_len] >= a[i]) r = mid_len; else l = mid_len + 1; }
-
模板2:查右边的那个数
int l = 1, r = len; while (l < r) { int mid_len = (l + r + 1) >> 1; if (last[mid_len] >= a[i]) r = mid_len - 1; else l = mid_len; }
-
二分查到这个数后需要进行特判
- 如果这个数比当前的
a[i]
大或相等,说明整个last
数组中没有比a[i]
严格小的数,那么直接更新这个last[r]
即可(其实这个last[r]
一定是last[1]
- 如果这个数比当前的
a[i]
小,需要特判这个数能否让len
进行扩增- 即如果这个数比当前
last
数组中最大的那个数(其实就是最后一个数)还要大,那么len
就可以+1
- 否则更新
last[r + 1]
为last[r + 1]
与a[r]
中的最小值
- 即如果这个数比当前
-
-
时间复杂度 O ( n log n ) \mathcal O(n \log{n}) O(nlogn)
-
AC代码
void solve() { int n; cin >> n; vector<int> a(n + 1); for (int i = 1; i <= n; i++) cin >> a[i]; int len = 0; vector<int> last(n + 1, 1e9); last[++len] = a[1]; for (int i = 2; i <= n; i++) { int l = 1, r = len; while (l < r) { int mid_len = (l + r + 1) >> 1; if (last[mid_len] >= a[i]) r = mid_len - 1; else l = mid_len; } if (last[r] >= a[i]) last[r] = a[i]; else { if (last[r + 1] == 1e9) len++; last[r + 1] = min(last[r + 1], a[i]); } } cout << len << "\n"; }
T2【二分】TODO
原题链接
大致题意是:给了n种药,每种药接下来的指定天数内每天都要吃一定的量,问在第几天吃的药的量小于一个指定的数值
我的思路
- 如果我们将从第一天到最后一天吃的药量进行求和并按照降序排序,可以发现从第一天开始
- 时间复杂度 O ( ) \mathcal O() O()
AC代码
bool check(int m, ll n, ll k, vector<pair<ll, ll>>& a)
{
ll sum = 0;
for (int i = 1; i <= n; i++)
if (a[i].first >= m)
sum += a[i].second;
else break;
return sum <= k;
}
void solve()
{
ll n, k;
cin >> n >> k;
vector<pair<ll, ll>> a(n + 1);
ll max_day = 0;
for (int i = 1; i <= n; i++)
{
ll day, num;
cin >> day >> num;
max_day = max(max_day, day);
a[i].first = day, a[i].second = num;
}
sort(a.begin() + 1, a.begin() + n + 1, [&](pair<ll, ll> x, pair<ll, ll> y) {
return x.first > y.first;
});
int l = 1, r = max_day + 1;
while (l < r)
{
int m = (l + r) >> 1;
if (check(m, n, k, a)) r = m;
else l = m + 1;
}
cout << r << "\n";
}
day09
比赛传送门
没写题解,留坑
day10
今天主要是出题,下面是我出题目的全部内容,具体OJ评测在CF上
T1【数学 | 排序 | 思维】*
原题:
AcWing 4653. 数位排序
改编思路:
单纯的数位之和没意思,多给几个选项输出最高排名即可。需要注意的是对于第二种方法理论上要开long long,同时第二种方法的时间复杂度无法在1秒内通过本题
有两种重新排位的方法
- 数位之和
- 每一个数位的2次方之积
时空限制
时间 1 s 1s 1s
空间 256 M B 256MB 256MB
题面
-
牛魔王在美好的高中时代情窦初开,从高一就开始暗恋班里的一个安静可爱的女生–小美。高三的一个明朗的午后,阳光正好,他决定表白了❤️🌹🎈🌹❤️
小美很上进,十分在意自己的考试排名,机智的牛魔王看着手里从咩霸那里偷来的两块水晶,想出了一个好方法来进行表白。那就是通过水晶修改世界的排名规则,让女孩的排名强行上升,以此来取悦小美。
已知两块水晶分别有不同的功效:
-
红色水晶的功效是:按照原始排名的数字的各数位之和进行重排名(当数位之和相等时,数值小的排在前面)
比如 2023 2023 2023 排在 602 602 602 前面,因为 2 + 0 + 2 + 3 = 7 < 6 + 0 + 2 = 8 2+0+2+3=7<6+0+2=8 2+0+2+3=7<6+0+2=8
-
紫色水晶的功效是:按照原始排名数字的 2 2 2 的每一个数位次方之积进行重排名(当指数之积相等时,数值小的排在前面)
比如 99 99 99 排在 100 100 100 后面,因为 2 9 × 2 9 > 2 1 × 2 0 × 2 0 2^9 \times 2^9>2^1 \times2^0 \times2^0 29×29>21×20×20
-
为了提高表白成功的概率,牛魔王需要选择合适的水晶(当然可以不选),来尽可能的让小美的排名升到最高。
请你帮助每次都考倒数第 K K K 名的牛魔王进行最佳决策,让小美的排名尽可能的高
输入
一行三个整数 K , N , M K,N,M K,N,M,均用一个空格隔开,分别代表倒数第 K K K 名,牛魔王的原始排名 N N N,小美的原始排名 M M M(保证数据合法)
输出
一个整数,表示修改后小美的最高排名
数据范围
对于 70 % 70\% 70% 的数据, 班级总人数 ≤ 1 0 3 班级总人数\le10^3 班级总人数≤103
对于 100 % 100\% 100% 的数据, 班级总人数 ≤ 1 0 6 班级总人数\le10^6 班级总人数≤106
样例
输入
1 13 2
输出
2
NOTE
牛魔王不需要进行任何选择,就能让小美保持住最高排名
时间复杂度计算:
本题不难发现第二种方法是一个幌子
虚晃一枪,因为次幂之积的指数其实就是数位之和,因此排序后的顺序是一样的如果没有发现这个性质直接进行模拟的话可以通过前 70 % 70\% 70% 的数据,时间复杂度是 O ( 10 × n log 2 n ) \mathcal O(10 \times n \log_2{n}) O(10×nlog2n)
但是!后面的三个数据点应该是过不去了,因为在 1 0 6 10^6 106 级别下,进行 s o r t sort sort 的时间复杂度理论上是 n log 2 n n \log_2{n} nlog2n,但是由于进行数位拆分以及算指数运算,常数会变得非常大。比如数位拆分均摊下来是 O ( 3 ) \mathcal O(3) O(3),快速幂均摊下来差不多也是 O ( 3 ) \mathcal O(3) O(3),于是 1 0 6 log 2 1 0 6 × 10 10^6 \log_2{10^6} \times10 106log2106×10,已经超过 1 1 1 秒了
还有一个坑点就是假如纯纯模拟,那么需要开 l o n g l o n g long\ long long long ,因为对于 1 0 6 10^6 106 的数据量,假设有一个数是 999999 999999 999999,那么 ( 2 9 ) 6 = 1 0 16 (2^9)^6=10^{16} (29)6=1016
代码实现:
TLE代码
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long ll;
ll qmi(ll a, ll b)
{
ll res = 1;
while (b)
{
if (b & 1) res = res * a;
a = a * a;
b >>= 1;
}
return res;
}
bool rule1(int x, int y)
{
int t1 = 0, t2 = 0;
for (int i = x; i; i /= 10) t1 += i % 10;
for (int i = y; i; i /= 10) t2 += i % 10;
if (t1 != t2) return t1 < t2;
else return x < y;
}
bool rule2(int x, int y)
{
ll t1 = 1, t2 = 1;
for (int i = x; i; i /= 10) t1 *= qmi(2, i % 10);
for (int i = y; i; i /= 10) t2 *= qmi(2, i % 10);
if (t1 != t2) return t1 < t2;
else return x < y;
}
void solve()
{
int k, n, m;
cin >> k >> n >> m;
// 全班人数
n = n + k - 1;
// 每个人的原始排名
vector<int> a(n + 1);
for (int i = 1; i <= n; i++)
a[i] = i;
// 数位之和规则下的排名
sort(a.begin() + 1, a.begin() + n + 1, rule1);
int res1;
for (int i = 1; i <= n; i++)
if (a[i] == m)
res1 = i;
// 2次幂之积规则下的排名
sort(a.begin() + 1, a.begin() + n + 1, rule2);
int res2;
for (int i = 1; i <= n; i++)
if (a[i] == m)
res2 = i;
cout << min(m, min(res1, res2)) << "\n";
}
int main()
{
solve();
return 0;
}
AC代码
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
bool rule1(int x, int y)
{
int t1 = 0, t2 = 0;
for (int i = x; i; i /= 10) t1 += i % 10;
for (int i = y; i; i /= 10) t2 += i % 10;
if (t1 != t2) return t1 < t2;
else return x < y;
}
void solve()
{
int k, n, m;
cin >> k >> n >> m;
// 全班人数
n = n + k - 1;
// 每个人的原始排名
vector<int> a(n + 1);
for (int i = 1; i <= n; i++)
a[i] = i;
// 数位之和规则下的排名
sort(a.begin() + 1, a.begin() + n + 1, rule1);
int res1;
for (int i = 1; i <= n; i++)
if (a[i] == m)
res1 = i;
cout << min(m, res1) << "\n";
}
int main()
{
solve();
return 0;
}
day11 & day12 & day13 & day14
比赛传送门
T1【数学】
原题链接
大致题意是:给定一个数,经过一定的演算后是否为质数
我的思路
- 最终的结果是两个质数之和然后整除2向下取整,不难发现最终的这个数除非是2和其他的质数,否则其他任何两个非2的质数之和一定是偶数,整除2以后还是质数,就一定不可能是质数,于是特判一下输入值是否为2即可
- 时间复杂度 O ( 1 ) \mathcal O(1) O(1)
- AC代码
void solve()
{
ll x;
cin >> x;
if (x == 1)
cout << "YES\n";
else
cout << "NO\n";
}
T2【数学 & 模拟】
原题链接
大致题意是:给定一个字符串,问其中至少以三个字符组成的字符子串中有单独存在的字符的种类数
我的思路
-
我们可以将单独存在的字符分为三种情况,即在开头;在中间;在结尾
举个例子比如HHHHGHHH,那么G作为开头,答案共2种;作为结尾,答案共三种;作为中间,答案共6种
我们将G左边连续的与它不同的字符个数记作L,右边的记作R,那么答案就是 L − 1 + R − 1 + L ∗ R L - 1 + R - 1 + L* R L−1+R−1+L∗R
于是我们枚举每一个字符,计算左右两边与他不相同的字符的个数代入上式计算求和即可
-
时间复杂度不太会计算,但是肯定是一次方级别的,姑且认为是 O ( n ) \mathcal O(n) O(n)
-
AC代码
void solve()
{
int n; cin >> n;
string s; cin >> s;
s = '0' + s;
ll res = 0;
for (int i = 1; i <= n; i++)
{
ll cntl = 0, pl = i - 1;
while (pl >= 1 && s[pl] != s[i])
pl--, cntl++;
ll cntr = 0, pr = i + 1;
while (pr <= n && s[pr] != s[i])
pr++, cntr++;
res += max(0ll, cntl - 1) + max(0ll, cntr - 1) + cntl * cntr;
}
cout << res << "\n";
}
感谢观看至此,为时两周的集训就完结撒花啦🌹🌹🌹文章来源地址https://www.toymoban.com/news/detail-540449.html
补题日志
- 2024.1.15 补充 day04&day05 T7
到了这里,关于算法集训日志的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!