第二周训练
训练主要内容:动态规划、数据结构
第一题 加一
题目1
对于一个数进行 m m m 次操作。每次操作中,将这个数的每一位加上1(9变成10),再组成新数。问操作后的结果。
思路1
如果通过模拟每一次操作,运算量较大会超时。
由于0-9每个数操作m次后得到的长度都可以计算,而且运算次数为
10
×
2
⋅
1
0
5
10 \times 2 \cdot 10 ^ 5
10×2⋅105 所以可以先计算0-9所有数字操作
m
m
m 次后得到的长度再计算每个数字的个数后累加。
计算0-9每个数操作
m
m
m 次后的长度可以通过动态规划。
状态:0-9中的数字
j
j
j ,操作
i
i
i 次
状态转移方程:
f
(
i
,
j
−
1
)
=
f
(
i
−
1
,
j
)
,
j
∈
[
1
,
9
]
f(i, j - 1) = f(i - 1, j), j \in [1,9]
f(i,j−1)=f(i−1,j),j∈[1,9]
f
(
i
,
9
)
=
f
(
i
−
1
,
0
)
+
a
(
i
−
1
,
1
)
f(i, 9) = f(i - 1, 0) + a(i - 1, 1)
f(i,9)=f(i−1,0)+a(i−1,1)
起始条件:
f
(
0
,
j
)
=
1
,
j
∈
[
0
,
9
]
f(0, j) = 1, j \in [0, 9]
f(0,j)=1,j∈[0,9]
代码1
#include<cstdio>
#include<cstring>
constexpr int maxm = 2e5 + 1, mod = 1e9 + 7;
int t;
//存放f,存放0-9每一位的个数
int a[maxm][10], num[10];
int main()
{
//起始条件
for (int i = 0; i < 10; i++)
{
a[0][i] = 1;
}
//状态转移方程
for (int i = 1; i <= maxm; i++)
{
for (int j = 1 ; j < 10; j++)
{
a[i][j - 1] = a[i - 1][j];
}
a[i][9] = (a[i - 1][0] + a[i - 1][1]) % mod;
}
scanf("%d", &t);
while (t--)
{
//清空数据
memset(num, 0, sizeof(num));
int n, m;
scanf("%d %d", &n, &m);
//计算每一位的个数
while (n > 0)
{
num[n % 10]++;
n /= 10;
}
int ans = 0;
for (int i = 0; i < 10; i++)
{
//不使用a[m][i] * num[i],可能导致溢出
for (int j = 0; j < num[i]; j++)
{
ans += a[m][i];
ans %= mod;
}
}
printf("%d\n", ans);
}
return 0;
}
第二题 跳跳
题目2
给一些整点,成为魔法阵。传送时,开始位于一个魔法阵上,通过魔法 ( A , B ) (A, B) (A,B) 可以从 ( x , y ) (x, y) (x,y) 传送到 ( x + A , y + B ) (x + A, y + B) (x+A,y+B) ,一次传送可以使用同一个魔法多次。问至少需要多少魔法才能从任意魔法阵直接传送到任意魔法阵。
思路2
由于可以使用同一个魔法多次,所以将所有需要的魔法都拆成最小的魔法,可以使需要的魔法最少。
最小的魔法可以通过求最大公约数来获取。求最大公约数可以通过内部函数 __
g
c
d
(
)
gcd()
gcd() 。存储和获取魔法是否已经存在可以使用
m
a
p
map
map ,魔法作为键,是否存在作为值。
代码2
#include<iostream>
#include<algorithm>
#include<utility>
#include<map>
#include<cmath>
using namespace std;
constexpr int maxn = 501;
//ans为需要的魔法数
int n, ans = 0;
//每个魔法阵的位置
int cx[maxn], cy[maxn];
//存储需要的魔法
map<pair<int, int>, bool> magic;
//获取两点之间传送需要的最小魔法
pair<int, int> getMagic(int p1, int p2)
{
int x = cx[p2] - cx[p1], y = cy[p2] - cy[p1];
//由于魔法不能使用负次数,所以两点任意传送需要相反的两个魔法。
//如果不获取绝对值,则两个魔法就会变成同一个魔法。
int gcd = abs(__gcd(x, y));
return make_pair(x / gcd, y / gcd);
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> cx[i] >> cy[i];
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
//在一个魔法阵之间传送
if (i == j)
{
continue;
}
//获取最小的魔法
pair<int, int> m = getMagic(i, j);
//如果还没有使用过这个魔法,就在map中标记需要这个魔法,并计数
if (magic.find(m) == magic.end())
{
ans++;
magic.insert(map<pair<int, int>, bool>::value_type(m, true));
}
}
}
cout << ans << endl;
return 0;
}
第三题 异或和或
题目3
有一个01序列,可以选择任意两个数,对其进行异或和或操作得到两个新数,再把两个新数返回原来的位置。问是否能将一个序列通过这个操作变成另一个序列。
思路3
异或 | 或 |
---|---|
0 ^ 0 = 0 | 0 | 0 = 0 |
1 ^ 0 = 1 | 1 | 0 = 1 |
1 ^ 1 = 0 | 1 | 1 = 1 |
由表可以看出,0和0不能产生1,1和1可以产生0,但不能产生两个0。
所以全是0的序列不能产生有1的序列,有1的序列不能产生全是0的序列。而其他情况都能产生。
因此只需要判断是否需要将全是0的序列转换为有1的序列或者将有1的序列转换为全是0的序列即可。
代码3
#include<iostream>
using namespace std;
int main()
{
int t;
cin >> t;
while (t--)
{
//原序列和新序列
string s, s2;
cin >> s >> s2;
//长度不同肯定不能转换
if (s.size() != s2.size())
{
cout << "NO" << endl;
continue;
}
//长度都为1时,只需要看两个序列是否相同
if (s.size() == 1)
{
cout << (s[0] == s2[0] ? "YES" : "NO") << endl;
}
else
{
//原序列1的个数和新序列1的个数
int o1 = 0, o2 = 0;
//计数
for (int i = 0; i < s.size(); i++)
{
if (s[i] == '1')
{
o1++;
}
if (s2[i] == '1')
{
o2++;
}
}
//原序列全是0而新序列有1
if(o1 == 0 && o2 > 0)
{
cout << "NO" << endl;
}
//原序列有1而新序列全是0
else if(o1 > 0 && o2 == 0)
{
cout << "NO" << endl;
}
//其他情况都可以
else
{
cout << "YES" << endl;
}
}
}
return 0;
}
第四题 01序列
题目4
求一个01序列的出现 k k k 次1的子序列的个数。
思路4
由于
0
≤
k
≤
1
0
6
0 \leq k \leq 10^6
0≤k≤106 ,如果枚举所有的子序列,为
O
(
n
2
)
O(n^2)
O(n2) ,会超时。
可以采用前缀和,只需要寻找前缀和相差
k
k
k 的起点和终点,即可得到一个符合要求的子序列。可以再将前缀和相同的个数存储下来,这样子序列的个数就是前缀和相差
k
k
k 的个数相乘再求和。只需要求解一次前缀和,时间复杂度为
O
(
n
)
O(n)
O(n) ,符合题目要求。
但是如果
k
=
0
k = 0
k=0 ,这个方法就不适用了。
前缀和为从第 1 1 1 项到第 n n n 的和。这里的和为1的个数。
代码4
#include<iostream>
#include<map>
using namespace std;
//前缀和
int sum[1000001];
int main()
{
int k;
cin >> k;
string s;
cin >> s;
long long cnt = 0;
//k = 0的情况
//对于每一个只有0组成的部分,能生成的字串个数是 n * (n + 1) / 2(n为0的个数)
//总个数即为所有个数之和
if (k == 0)
{
//记录0的个数
long long temp = 0;
for (int i = 0; i < s.size(); i++)
{
if (s[i] == '0')
{
temp++;
}
else
{
cnt += temp * (temp + 1) / 2;
temp = 0;
}
}
cnt += temp * (temp + 1) / 2;
}
//用前缀和
else
{
//前缀和
int temp = 0;
//记录前缀和相同的个数
map<int, int> m;
//循环会漏掉零个元素的前缀和,单独补上
m[0] = 1;
for (int i = 0; i < s.size(); i++)
{
if (s[i] == '1')
{
temp++;
}
//记录前缀和,其实前缀和完全不需要记录了,因为并没有用到
sum[i + 1] = temp;
//更新前缀和的个数
m[sum[i + 1]]++;
}
//求前缀和相差k的个数的积,并求和
for (int i = 0; i < s.size(); i++)
{
cnt += m[i] * m[i + k];
}
}
cout << cnt << endl;
return 0;
}
第五题 出栈序列判断
题目5
入栈序列为 1 , 2 , ⋯ , n 1, 2, \cdots, n 1,2,⋯,n ,给一个出栈序列。问如何进行入栈和出栈操作,可以产生给出的出栈序列。
思路5
由于入栈序列已经给出,递增。所以只需要一直入栈直到顶部的数与出栈序列当前的数相同,进行出栈即可。
代码5
#include<cstdio>
constexpr int maxn = 100005;
//栈中元素个数,模拟栈用的数组
int cnt = 0, s[maxn];
//入栈
void push(int number)
{
s[cnt++] = number;
}
//出栈
void pop()
{
cnt--;
}
//获取顶部元素
int top()
{
return s[cnt - 1];
}
//判断栈是否为空
bool empty()
{
return cnt == 0;
}
int main()
{
//now为当前要入栈的元素
int n, t, now = 1;
scanf("%d", &n);
//每次循环出栈一次
for (int i = 1; i <= n; i++)
{
//获取出栈序列当前元素
scanf("%d", &t);
//顶部元素不为出栈序列当前元素时就入栈
while (empty() || top() < t)
{
push(now);
printf("push %d\n", now);
now++;
}
//相同就出栈
if (top() == t)
{
pop();
printf("pop\n");
}
}
return 0;
}
第六题 序列维护
题目6
有一个序列,需要支持插入、删除和查询操作。
思路6
直接用vector模拟即可。
代码6
#include<iostream>
#include<vector>
using namespace std;
int main()
{
int m;
//序列
vector<int> v;
cin >> m;
while (m--)
{
string op;
cin >> op;
//插入
if (op == "insert")
{
int x, y;
cin >> x >> y;
v.insert(v.begin() + x, y);
}
//查找
else if (op == "query")
{
int k;
cin >> k;
cout << v[k - 1] << endl;
}
//删除
else
{
int x;
cin >> x;
v.erase(v.begin() + x - 1);
}
}
return 0;
}
第七题 网格判断
题目7
有一个 N × N N \times N N×N 的网格,每个正方形为黑色或白色。判断网格是否满足:
- 每行的黑色方块数与白色方块数相同。
- 每列的黑色正方形数与白色方块数相同。
- 没有行或列具有 3 3 3 个及以上相同颜色的连续正方形。
思路7
模拟,逐条判断每个条件即可。
代码7
#include<iostream>
using namespace std;
constexpr int maxn = 26;
int n;
//存放网格
char grid[maxn][maxn];
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
cin >> grid[i][j];
}
}
for (int i = 1; i <= n; i++)
{
//这一列白色的个数、这一列黑色的个数、这一行白色的个数、这一行黑色的个数
int cwcnt = 0, cbcnt = 0, rwcnt = 0, rbcnt = 0;
for (int j = 1; j <=n; j++)
{
//判断行
if (grid[i][j] == 'B')
{
rbcnt++;
}
else
{
rwcnt++;
}
//判断列
if (grid[j][i] == 'B')
{
cbcnt++;
}
else
{
cwcnt++;
}
}
//行或列有黑白色不相等
if (cwcnt != cbcnt || rwcnt != rbcnt)
{
cout << 0 << endl;
return 0;
}
}
for (int i = 1; i <= n; i++)
{
//判断有没有三个连续相同
for (int j = 3; j <= n; j++)
{
//行
if (grid[i][j - 2] == grid[i][j] && grid[i][j - 1] == grid[i][j])
{
cout << 0 << endl;
return 0;
}
//列
if (grid[j - 2][i] == grid[j][i] && grid[j - 1][i] == grid[j][i])
{
cout << 0 << endl;
return 0;
}
}
}
cout << 1 << endl;
return 0;
}
第八题 整齐的数组
题目8
有一组数和一个整数 k k k 。对数组操作:选择数组的一个数减去 k k k 。进行若干次操作后,所有数都变成同一个数。问最大的 k k k 。
思路8
如果能变成同一个数,那肯定可以变成原来的最小的数。找到所有数和最小数的差的最大公约数,这个数即为 k k k 。
代码8
#include<iostream>
#include<algorithm>
using namespace std;
constexpr int maxn = 40 + 2;
int t, n;
//存储数
int a[maxn];
int main()
{
cin >> t;
while (t--)
{
cin >> n;
//m存放最小数,ans是k
int m = 0x3f3f3f3f, ans = -1;
//找最小数
for (int i = 1; i <= n; i++)
{
cin >> a[i];
m = min(a[i], m);
}
//求最大公约数
for (int i = 1; i <= n; i++)
{
//求这个数与最小数的差
int sub = a[i] - m;
//这个数不是最小数
if (sub > 0)
{
//这是第一个数时,最大公约数就是自己
if (ans == -1)
{
ans = sub;
}
else
{
ans = __gcd(ans, sub);
}
}
}
cout << ans << endl;
}
return 0;
}
第九题 删删
题目9
给定一个字符串,你可以删除多个(可以是 0 0 0 ) 相同 的字符,这样操作之后,你能否得到一个回文串?如果能,求最小化删除的个数。
思路9
循环判断每一个字符是否为需要删除的数组。然后分别从字符串开头和字符串结尾向中间比对字符。如果有不相同的字符,说明这个字符需要删除才可能成为回文串。如果删去的字符都是判断的字符,就说明删除这个字符可以使字符串变成回文串。再求最小值即可。
代码9
#include<iostream>
using namespace std;
//无穷大
constexpr int inf = 0x3f3f3f3f;
int t;
int main()
{
cin >> t;
while (t--)
{
int n, ans = inf;
string s;
cin >> n >> s;
//循环每一个字符
for (char c = 'a'; c <= 'z'; c++)
{
//开头和结尾的下标,需要删除的字符的个数
int l = 0, r = n - 1, cnt = 0;
//逐个判断
while (l < r)
{
//出现不同字符
if (s[l] != s[r])
{
//删去是当前循环的字符
if (s[l] == c)
{
l++;
cnt++;
continue;
}
else if (s[r] == c)
{
r--;
cnt++;
continue;
}
//都不是那么这个字符不行
else
{
cnt = inf;
break;
}
}
//向中间移动
l++;
r--;
}
//更新答案
ans = min(ans, cnt);
}
cout << (ans == inf ? -1 : ans) << endl;
}
return 0;
}
第十题 快快变大
题目10
给定一个长度为 n n n 的数组,进行 n − 1 n − 1 n−1 次操作,每次选择一个下标 x x x ,将 a x a_x ax 和 a x + 1 a_{x + 1} ax+1 合并成 a x × a x + 1 m o d 1000003 a_x \times a_{x + 1} \space mod \space 1000003 ax×ax+1 mod 1000003 ,并获得 ( a x − a x + 1 ) 2 (a_x - a_{x + 1}) ^ 2 (ax−ax+1)2 分。当数组只剩一个数时停止。求最大分数。
思路10
这是一道动态规划。
状态:从
i
i
i 合并到
j
j
j 得到的最大分数
状态转移方程:
f
(
i
,
j
)
=
m
a
x
(
f
(
i
,
k
)
+
f
(
k
+
1
,
j
)
+
(
p
(
i
,
k
)
−
p
(
k
+
1
,
j
)
)
2
)
f(i, j) = max(f(i, k) + f(k + 1, j) + (p(i, k) - p(k + 1, j)) ^ 2)
f(i,j)=max(f(i,k)+f(k+1,j)+(p(i,k)−p(k+1,j))2)
p
(
i
,
j
)
\space \space p(i, j)
p(i,j) 是
i
i
i 到
j
j
j 的乘积,
k
k
k 是合并的下标
起始条件:
f
(
i
,
i
)
=
0
f(i, i) = 0
f(i,i)=0
代码10
#include<iostream>
//会溢出,要用long long
#define int long long
using namespace std;
constexpr int maxn = 305, mod = 1000003;
int n;
//数组,p,dp的结果f
int a[maxn], p[maxn][maxn], ans[maxn][maxn];
signed main()
{
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
//求p的起始条件
p[i][i] = 1;
p[i][i - 1] = 1;
}
//求p的结果
for (int i = 1; i <= n; i++)
{
for (int j = i; j <= n; j++)
{
p[i][j] = (p[i][j - 1] * a[j]) % mod;
}
}
//枚举区间长度
//要先求小区间的结果才能得到大区间的结果
for (int l = 2; l <= n; l++)
{
for (int i = 1; i <= n - l + 1; i++)
{
//求j的值
int j = i + l - 1;
//枚举k,从i到j - 1
for (int k = i; k < j; k++)
{
//状态转移方程
ans[i][j] = max(ans[i][j], ans[i][k] + ans[k + 1][j] + (p[i][k] - p[k + 1][j]) * (p[i][k] - p[k + 1][j]));
}
}
}
cout << ans[1][n] << endl;
return 0;
}