试题A: 九进制转十进制
2 * 9 ^ 0 + 2 * 9 ^ 1 + 0 + 2 * 9 ^ 3 = 1478
试题B: 顺子日期
很多人在讨论 012 算不算顺子,不算的话答案应该是 4
年份 2022 是不变的,而且不可能搭上顺子,所以只考虑后四位即可
可能搭上顺子的月份有:
1月:0120 ~0129 共 10 个,顺子是 012 (其中 0123 可以认为顺子是 123)
10月:1012,顺子是 012
11月:1123,顺子是 123
12月:1230,1231,顺子是 123
一共 14 个
试题C: 刷题统计
暴力模拟的话稳稳超时,利用除法和取余
#include<iostream>
using namespace std;
typedef long long ll;
ll a, b, n;
int main()
{
ios::sync_with_stdio(false);
cin >> a >> b >> n;
ll x = 5 * a + 2 * b;
ll y = n % x;
ll count = n / x;
ll count1 = 0;
ll ans = 0;
while (ans < y) {
if (count1 <= 5) {
ans += a;
count1++;
}
else {
ans += b;
count1++;
}
}
cout << 7*count + count1 << '\n';
return 0;
}
试题D: 修剪灌木
找规律:来回最长max(i-1, n - i) * 2
#include<iostream>
using namespace std;
int main()
{
ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
cin >> n;
for (int i = 1; i <=n; i++) {
cout << max(i-1, n - i) * 2 <<'\n';
}
return 0;
}
试题E: X进制减法
越界取模,
预算律(***重点***)
(a + b) % p = (a % p + b % p) % p
(a - b) % p = (a % p - b % p) % p
(a * b) % p = (a % p * b % p) % p
a ^ b % p = ((a % p) ^ b) % p
思路:(贪心)
我们先来看一下每一位上的数的权值是由什么决定的,就拿题目中给的数来说吧,
321,3所在的位是8进制,2所在的位是10进制,1所在的位是2进制,
很显然可以知道每2个最低位可以进一个第二位,每10个第二位可以进一个第一位,
也就是每2 * 10个第三位可以进一个第一位,写到这我们或许会发现,
****(第i位的权值其实就是比第i位低的位上的进制之积)****
由于我们已经知道A比B大了,为了使A - B尽可能地小,我们应该使得高位权值尽可能地小,
我们可以忽略低位权值产生的影响,为什么呢?因为高位权值如果下降1,那么比所有低位加起来产生的影响都大,
所以我们的贪心策略就是降低高位上的权值,结合我们刚才对影响权值因素的分析我们可以知道,
当低位上每一位数的进制都取到最小时,这个时候高位的权值最小,因为每一位都给出了两个数a和b,
因为进制不可能小于等于a或者小于等于b,
****所以我们只要令该位的进制为a和b的最大值加1即可。****
#include<iostream>
using namespace std;
typedef long long ll;
const int maxn = 100000;
const int mod = 1000000007;
int a[maxn], b[maxn];
ll base=1, ans;
int main()
{
ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int maxn;
cin >> maxn;
int m,n;
cin >> m;
for (int i = m - 1; i >= 0; i--) { //逆序输入
cin >> a[i];
}
cin >> n;
for (int i = n-1; i>=0; i--) {
cin >> b[i];
}
int p;
for (int i = 0; i < max(m, n); i++) {
p = max(max(a[i], b[i]) + 1, 2); //p为该位进制,因为每一数位上的数字要小于其进制,所以取max
ans = (ans + (a[i] - b[i]) * base) % mod;
base = (base * p) % mod; //base为该位权值
}
cout << ans % mod << '\n';
return 0;
}
试题F: 统计子矩阵
法1:使用前缀和+滑动窗口(适用于解决矩阵)
使用纵向前缀和+二重循环枚举上下边界+滑动窗口左右边界实现
#include<iostream>
using namespace std;
typedef long long ll;
const int maxn = 5e2 + 10;
int n, m,k;
ll a[maxn][maxn];
ll ans;
int main()
{
ios::sync_with_stdio(false);
std::cin.tie(nullptr);
cin >> n >> m >> k;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> a[i][j];
//维护****纵向前缀和****
a[i][j] += a[i - 1][j];
}
}
for (int i = 1; i <= n; i++) { //遍历上边界
for (int j = i; j <= n; j++) { //遍历下边界
int l = 1, r = 1;
int sum = 0;
for (r = 1; r <= m; r++) {
sum += a[j][r] - a[i - 1][r]; // 加上右端点处的和
while (sum > k) {
sum -= a[j][l] - a[i - 1][l]; //减去移出去的左端点处的和
l++;
}
ans += r - l + 1; // 方法数就是找到的区间大小累加
}
}
}
cout << ans << '\n';
return 0;
}
(**重点**)试题G: 积木画
思路:动态规划:最终状态由前一个状态的多种形式决定
要摆出 n 列方格,
1.可以在 n - 1 列方格后加一个竖着的 I 形积木,
2.也可以在 n - 2 方格后加两个横着的 I 形积木,
3.也可以在 n - 3 列方格上后加 两个 L 形积木的两种摆法,
4.在n-4,n-6后(1)可以先放一个 L 形,再连续放若干个横着的 I 形积木,末尾再放一个L形,(2)也可反L形式,再连续放若干个横着的 I 形积木,末尾再放一个反L 形
5.在n-5,n-7后(1)可以先放一个 L 形,再连续放若干个横着的 I 形积木,末尾再放一个倒置L形,(2)可以先放一个 反L 形,再连续放若干个横着的 I 形积木,末尾再放一个L形
① dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3] * 2 + dp[i - 4] * 2+ dp[i - 5] * 2 + …… + * dp[1] * 2
② dp[i - 1] = dp[i - 2] + dp[i - 3] + dp[i - 4] * 2 + dp[i - 4] * 2 + dp[i - 5] * 2 + …… + *dp[1] * 2
① - ② 得出 dp[i] - dp[i - 1] = dp[i - 1] + dp[i - 3] 即 dp[i] = dp[i - 1] * 2 + dp[i - 3]
步骤:由小状态分析,分析直到此小状态一定只能由比它小的状态组合而成,得到状态表达式,化简
#include <iostream>
using namespace std;
typedef long long ll;
const ll MOD = 1000000007;
const int N = 10000005;
int n;
ll dp[N];
int main() {
cin >> n;
dp[1] = 1;
dp[2] = 2;
dp[3] = 5;
for (int i = 4; i <= n; i++) {
dp[i] = (dp[i - 1] * 2 + dp[i - 3]) % MOD;
}
cout << dp[n] << endl;
return 0;
}
试题H:扫雷
引入:当数组过大时,1.采用hash表(***重点***)2.map<pair<int, int>, int>mp;
思路:深度度优先搜索
#include<iostream>
#include<cstring>
using namespace std;
typedef long long LL;
const int M = 999997, Base = 1e9 + 1;
int res, hash2[M], hash3[M], book[M];
LL hash1[M];
LL get_key(int x, int y) //一重hash
{
return (LL)x * Base + y;
}
int find(LL x) //二重hash
{
int t = (x % M + M) % M;
while (hash1[t] != -1 && hash1[t] != x)
{
t++;
if (t == M)
t = 0;
}
return t;
}
bool judge(int x1, int y1, int x2, int y2, int r) //判断结点是否在半径内
{
int d = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
return d <= r * r;
}
void dfs(int x, int y, int r)
{
for (int i = -r; i <= r; i++)
for (int j = -r; j <= r; j++)
{
int dx = x + i, dy = y + j;
LL t = get_key(dx, dy);
if (hash2[find(t)] && judge(x, y, dx, dy, r) && !book[find(t)])
{
res += hash2[find(t)];
book[find(t)] = 1;
dfs(dx, dy, hash3[find(t)]);
}
}
return;
}
int main()
{
int n, m;
cin >> n >> m;
memset(hash1, -1, sizeof hash1);
for (int i = 0; i < n; i++)
{
int x, y, r;
cin >> x >> y >> r;
LL t = get_key(x, y);
hash1[find(t)] = t;
hash2[find(t)]++;
hash3[find(t)] = max(r, hash3[find(t)]);
}
for (int i = 0; i < m; i++)
{
int x, y, r;
cin >> x >> y >> r;
dfs(x, y, r);
}
cout << res;
return 0;
}
试题:李白打酒加强版
思路:状态压缩动态规划,错解(不能解决)
#include<iostream>
using namespace std;
const int mod = 1000000007;
typedef long long ll;
ll ans = 0;
int main()
{
ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m;
cin >> n >> m;
for (ll i = 0; i < (1 << (n + m-1)); i++) { (1<<(n+m-1))=(n+m-1)个1
int top_1 = 0;
int top_0 = 0;
int num = 2;
for (ll j = 0; j < n+m-1; j++) {
if (i & (1 << j)) { //判断二进制i从右数第j+1位是否是1
top_1++;
num *= 2;
}
else {
top_0++;
num -= 1;
}
}
if (top_1 == n && top_0 == m-1 && num == 1) {
ans = (ans + 1) % mod;
}
}
cout << ans % mod << '\n';
return 0;
}
思路:三维数组(可以解决)
采用未遇见的花和店的减少,而非采用遇见花和店的增加
但同样是由小状态推至最终状态
所以我们可以写出递推式:
一遇见花了:dp[i][j - 1][k - 1] += dp[i][j][k];
二遇见店了:dp[i - 1][j][k << 1] += dp[i][j][k];
#include<iostream>
using namespace std;
const int mod = 1000000007;
const int maxn = 105; //因为酒要喝完,所以三维最大为100,数组不会超限
int dp[maxn][maxn][maxn]; //一维记录还有n个店未遇见,二维记录还有m个花未遇见,三维记录还有多少酒
int n, m;
int main()
{
ios::sync_with_stdio(false);
std::cin.tie(nullptr);
cin >> n >> m;
dp[n][m][2] = 1;
for (int i = n; i >= 0; i--) {
for (int j = m; j >= 0; j--) {
for (int k = m; k >= 0; k--) { //第三维是--因为最后喝酒的状态是由多到无的过程,
if (2 * k <= m && i >= 1)
dp[i - 1][j][k << 1] =(dp[i - 1][j][k << 1] + dp[i][j][k])%mod;
if (j >= 1 && k >= 1)
dp[i][j - 1][k - 1] = (dp[i][j - 1][k - 1] + dp[i][j][k]) % mod;
}
}
}
cout << dp[0][1][1]%mod << '\n';
return 0;
}
采用遇见花和店的增加
#include<iostream>
using namespace std;
const int mod = 1000000007;
const int maxn = 105; //因为酒要喝完,所以三维最大为100,数组不会超限
int dp[maxn][maxn][maxn]; //一维记录还有n个店未遇见,二维记录还有m个花未遇见,三维记录还有多少酒
int n, m;
int main()
{
ios::sync_with_stdio(false);
std::cin.tie(nullptr);
cin >> n >> m;
dp[0][0][2] = 1;
for (int i = 0; i<= n; i++) {
for (int j = 0; j<= m; j++) {
for (int k =m ; k >= 0; k--) { //第三维是--因为最后喝酒的状态是由多到无的过程,
if (2 * k <= m )
dp[i+1][j][k << 1] = (dp[i+1][j][k << 1] + dp[i][j][k]) % mod;
if ( k >= 1)
dp[i][j+1][k - 1] = (dp[i][j+1][k - 1] + dp[i][j][k]) % mod;
}
}
}
cout << dp[n][m-1][1] % mod << '\n';
return 0;
}
试题J:砍竹子
1.贪心骗分(不能通过全部)
#include<iostream>
#include<math.h>
using namespace std;
typedef long long ll;
const int maxn = 2e5 + 10;
ll a[maxn];
int ans = 0;
int main()
{
ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
while (1) {
int flag = 0;
for (int i = 0; i < n; i++) {
if (a[i] > a[flag])
flag = i;
}
if (a[flag] == 1)
break;
ll flag1 = a[flag];
for (int i = flag; a[i] == flag1; i++) {
a[i] = sqrt(a[i] / 2 + 1);
}
ans++;
}
cout << ans << '\n';
return 0;
}
2.正解:
分析:
1.先来说一个贪心策略,我们优先选择砍所剩竹子中高度最大的竹子,
因为无论我们怎么砍高度低的竹子都不可能使得高度低的竹子高度变高,从而能够和高度高的竹子一块被砍,
相反的,我们砍完高度高的竹子后由于高度变低,所以可能会跟原来高度低的竹子一块被砍,所以这种贪心策略显然是正确的。
而且高度相同的连续竹子一定要放在一起砍,这是显然的,一次可以砍完的事情为什么非要分几次呢?
有了这个策略我们再来看一下一棵竹子最多会被砍多少次,你可以先按竹子高度最高1e18来算,
发现他经过6次就可以砍成高度为1的竹子,也就是说每棵竹子被砍的次数都不会超过6.
2.那我们可以开一个优先队列,里面存pair类型,第一维是竹子的高度,第二维是竹子的编号,
那么我们先把所有的竹子放入优先队列,每次取出一棵竹子,并记录其编号,
直到取到一棵竹子高度不等于前一棵竹子的高度或者编号与前一棵竹子编号不是相连的,这个时候就将砍的次数 + 1.
注意由于高度是从高到低排的,我没有用结构体排序,所以pair两维都是按照从高到低的规则来进行排序的,
所以先出队列的就是编号较大的,只需要进行一下判断高度是否相等以及编号是否连续即可。
还有一点需要注意的就是,我们每次取出队头竹子,然后把这个竹子的高度和编号记录一下,
用于判断后续竹子是否可以和当前竹子一块被砍,然后就可以直接把当前竹子砍掉并讲砍完后的高度连同其编号一同放入优先队列(前提是竹子被砍后高度不为1),直至队空为止。
3.最后分析一下复杂度:每棵竹子最多进6次队列,共有n棵竹子,由于队列中最多同时有n棵竹子,所以每次入队都是o(logn)的,
所以说总的复杂度就是6 * n * logn,是可以通过所有数据的。
(注)手动开启优化可过
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma GCC optimize(fast)
#include<iostream>
#include<queue>
#include<math.h>
using namespace std;
typedef long long ll;
int n;
ll ans;
priority_queue<pair<ll, int>>q; //优先队列它的第一个元素总是它所包含的元素中最大的。
int main()
{
ios::sync_with_stdio(false);
std::cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
ll t;
cin >> t;
if (t != 1)
q.push({t,i}); //已自动从大到小排序
}
while (!q.empty()) {
ll t = q.top().first; //记录当前竹子高度
int r = q.top().second; //记录编号
q.pop();
ll l = sqrt(t / 2 + 1);
if (l != 1)
q.push({ l,r});
while (!q.empty() && q.top().first == t && q.top().second == r - 1) {
r--;
q.pop();
if (l != 1)
q.push({ l,r });
}
ans++;
}
cout << ans << '\n';
return 0;
}
优化:
可以先存下来第i棵树还剩j次砍为1时的高度记录为f[i][j],说的有点绕,我举个例子:
比如第i棵树高度为15,那么第一次砍完后高度为2,第二次砍完后高度为1。
那么还剩一次就能砍为1的高度为2,还剩两次就能砍为1的高度为15.
明白了f数组的含义后我们就能够对上面的问题进行简化了,首先可以知道的一点就是当前仅当两棵树在同一层才有可能被同时砍,
能被同时砍不仅要求在同一层,还需要要求高度相同且编号相邻,所以我们可以直接遍历每一层,
只要发现有相邻的两棵树高度相同我们的砍树次数就可以减少1,
一开始的砍树次数就是所有的树都一次一次地砍为1所需要的次数和,
利用这样的方法我们就可以优化刚才进入优先队列的o(logn)的复杂度,所以总的复杂度就是6n
#include<iostream>
#include<queue>
#include<math.h>
using namespace std;
typedef long long ll;
const int maxn = 2e5 + 10;
ll f[maxn][8],s[8];
ll ans;
int main()
{
ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
ll x;
cin >> x;
ll t = 0;
while (x != 1) {
s[++t] = x; //s[i]记录当前竹子被砍i-1次后的长度
x = sqrt(x / 2 + 1);
}
ans += t;
for (int j = 1; t > 0; j++, t--) {
f[i][j] = s[t];
}
}
for (int i = 1; i <= 7; i++) { //枚举次数
for (int j = 2; j <= n; j++) { //枚举竹子编号
if (f[j][i] && f[j][i] == f[j - 1][i])
ans--;
}
}
cout << ans << '\n';
return 0;
}