第五周
第一题 碰撞2
题目1
在
x
y
xy
xy 坐标系中有
N
N
N 个人,第
i
i
i 个人的位置是
(
X
i
,
Y
i
)
(X_i,Y_i)
(Xi,Yi) ,并且每个人的位置都不同。
有一个由
L
L
L 和
R
R
R 组成的长为
N
N
N 的字符串
S
S
S,
S
i
=
R
S_i = R
Si=R 代表第
i
i
i 个人面向右,
S
i
=
L
S_i = L
Si=L 代表第
i
i
i 个人面向左。
现在所有人开始朝着他们各自面向的方向走,即面向右
x
x
x 就增,面向左
x
x
x 就减。
把两个人对向行走到一个位置称为一次碰撞。请问如果人们可以无限走下去,会有人产生碰撞吗?
思路1
只有
y
y
y 相同的人才有可能相撞。
对于
y
y
y 相同的一些人,只有两种情况不会发生碰撞:
- 全部朝一个方向
- 左边一部分人朝左,右边一部分人朝右
所以只需要从左向右判断这些人是不是先朝左然后再朝右即可。
由于坐标的范围很大,不可能对每一个
y
y
y 都创建一个对象,所以可以用map来记录。
代码1
#include<iostream>
#include<vector>
#include<algorithm>
#include<map>
using namespace std;
constexpr int MAXN = 2e5 + 5;
//map,键为y左边,值为所有人的x坐标和朝向
map<int, vector<pair<int, char> > > m;
int cx[MAXN], cy[MAXN];
//判断是否碰撞
bool judge()
{
//遍历每一个y值
for (pair<int, vector<pair<int, char> > > p : m)
{
//对所有人的位置排序,保证站在左边的人在前面
sort(p.second.begin(), p.second.end());
//记录当前队列里面的人是朝左还是朝右
//先按照朝左来判断。如果出现了朝右的,后面所有人都必须朝右
bool left = true;
//前一个人的位置,防止出现开始时就站在一起的情况
int last = -1;
for (int i = 0; i < p.second.size(); i++)
{
//判断有没有人开始就站在一起
if (last == p.second[i].first)
{
return true;
}
last = p.second[i].first;
//还是按照朝左判断时
if (left)
{
//遇到了朝右的
if (p.second[i].second != 'L')
{
//更新标记
left = false;
}
}
//按照朝右判断时
else
{
//这时还有朝左的肯定会碰撞
if (p.second[i].second != 'R')
{
return true;
}
}
}
}
//遍历完没有问题就不会碰撞
return false;
}
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> cx[i] >> cy[i];
}
string s;
cin >> s;
for (int i = 1; i <= n; i++)
{
m[cy[i]].push_back({ cx[i], s[i - 1] });
}
cout << (judge() ? "Yes" : "No") << endl;
return 0;
}
第二题 优美!最长上升子序列
题目2
给定一个数组。派派希望从中选择最长的递增的子序列,此外还要满足后一个下标能整除前一个下标。
思路2
还是通过动态规划来解决。
状态:以
i
i
i 结尾的最长的递增的子序列
状态转移方程:
f
(
i
)
=
m
a
x
(
f
(
j
)
+
1
)
,
i
m
o
d
j
=
0
,
a
i
>
a
j
f(i) = max(f(j) + 1), \space i \mod j = 0, \space a_i > a_j
f(i)=max(f(j)+1), imodj=0, ai>aj
起始条件:
f
(
i
)
=
1
,
i
∈
[
1
,
n
]
f(i) = 1, i \in [1, n]
f(i)=1,i∈[1,n]
由于题目要求下标整除,所以可以用前面的下标去推后面所有能整除的下标,这样更加快速。
代码2
#include<iostream>
#define int long long
using namespace std;
constexpr int MAXN = 1e6 + 5;
int a[MAXN];
//dp结果
int ans[MAXN];
signed main()
{
int t;
cin >> t;
//用于输出格式
bool first = true;
while (t--)
{
int n;
//当前询问的答案
int tans = 1;
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
ans[i] = 1;
}
//dp
for (int i = 1; i <= n; i++)
{
//寻找所有能整除i的j
for (int j = i * 2; j <= n; j += i)
{
//转移方程
if (a[j] > a[i])
{
ans[j] = max(ans[j], ans[i] + 1);
//更新答案
tans = max(ans[j], tans);
}
}
}
//输出
if (first)
{
cout << tans;
first = false;
}
else
{
cout << ' ' << tans;
}
}
return 0;
}
第三题 巨大的牛棚
题目3
农夫约翰想要在他的正方形农场上建造一座正方形大牛棚。找一个能够让他在空旷无树的地方修建牛棚的地方。他的农场划分成 n × n n \times n n×n 的方格。计算并输出,在他的农场中,不需要砍树却能够修建的最大正方形牛棚。
思路3
因为农场较大,枚举会比较费时。所以可以考虑使用二分答案。
将边长进行二分,再枚举每一个位置进行判断即可。
此外计算一块地上数的个数也很费时。而使用矩阵前缀和可以快速得到结果。
矩阵前缀和计算方法:
s
u
m
(
i
,
j
)
=
s
u
m
(
i
−
1
,
j
)
+
s
u
m
(
i
,
j
−
1
)
−
s
u
m
(
i
−
1
,
j
−
1
)
+
a
(
i
,
j
)
sum(i, j) = sum(i - 1, j) + sum(i, j - 1) - sum(i - 1, j - 1) + a(i, j)
sum(i,j)=sum(i−1,j)+sum(i,j−1)−sum(i−1,j−1)+a(i,j)
通过前缀和得到一块区域和的方法:
p
s
u
m
(
x
,
y
,
l
)
=
s
u
m
(
x
+
l
,
y
+
l
)
−
s
u
m
(
x
+
l
,
y
)
−
(
x
,
y
+
l
)
+
s
u
m
(
x
,
y
)
psum(x, y, l) = sum(x + l, y + l) - sum(x + l, y) - (x, y + l) + sum(x, y)
psum(x,y,l)=sum(x+l,y+l)−sum(x+l,y)−(x,y+l)+sum(x,y)
代码3
#include<iostream>
using namespace std;
constexpr int MAXN = 1e3 + 5;
bool a[MAXN][MAXN];
//前缀和
int sum[MAXN][MAXN];
//判断答案
bool judge(int n, int l)
{
for (int i = 1; i + l <= n; i++)
{
for (int j = 1; j + l <= n; j++)
{
if (sum[i + l][j + l] + sum[i][j] - sum[i][j + l] - sum[i + l][j] == 0)
{
return true;
}
}
}
return false;
}
int main()
{
int n, t;
cin >> n >> t;
for (int i = 1; i <= t; i++)
{
int x, y;
cin >> x >> y;
a[x][y] = true;
}
//计算前缀和
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + a[i][j];
}
}
//二分
int l = 0, r = n, ans = 0;
while (l <= r)
{
int mid = (l + r) / 2;
if (judge(n, mid))
{
l = mid + 1;
//更新答案
ans = mid;
}
else
{
r = mid - 1;
}
}
cout << ans << endl;
return 0;
}
第四题 高利贷
题目4
贷款机构利率按月累计,贷款人在贷款期间每月偿还固定的分期付款金额。
给出贷款的原值为
n
n
n,分期付款金额
m
m
m 和分期付款还清贷款所需的总月数
k
k
k,求该贷款的月利率
p
p
p。
思路4
这道题无法直接解出答案。但是题目只要求误差小于
1
×
1
0
−
6
1 \times 10^{-6}
1×10−6,所以可以通过二分来计算答案。
使用二分的答案,计算按答案得到的原款数,再进行比较。
公式为
n
=
∑
i
=
1
k
m
(
1
+
p
)
i
n = \sum_{i = 1}^k\frac m {(1 + p)^i}
n=∑i=1k(1+p)im
代码4
#include<iostream>
#include<iomanip>
using namespace std;
//误差
constexpr double MISS = 1e-7;
//判断二分答案
bool judge(double n, double m, int k, double p)
{
//每月利率的倍数
double ratio = 1 + p;
//当前月利率
double pk = 1;
//计算得到的原款
double sum = 0;
//循环计算每月原款相当于原款数
for (int i = 1; i <= k; i++)
{
pk *= ratio;
sum += m / pk;
}
return sum >= n;
}
int main()
{
double n, m, ans;
int k;
cin >> n >> m >> k;
//二分
double l = 0, r = 5;
while (r - l >= MISS)
{
double mid = (l + r) / 2;
if (judge(n, m, k, mid))
{
l = mid;
ans = mid;
}
else
{
r = mid;
}
}
cout << setiosflags(ios::fixed) << setprecision(6) << ans << endl;
return 0;
}
第五题 背包
题目5
有一个背包,背包的体积为
w
w
w,有
n
n
n 个物品,每一个物品的体积为
a
i
a_i
ai。cc希望将其中的一些物品放入他的背包中,他希望这些物品的体积之和至少是背包体积的一半,并且不超过背包的体积,即
⌈
w
2
⌉
≤
s
u
m
≤
w
\lceil \frac w2 \rceil \leq sum \leq w
⌈2w⌉≤sum≤w。
判断这些物品中有没有符合条件的物品组合,如果有输出"YES", 没有输出"NO"。
思路5
遍历每一件物品。如果有物品大于体积的一半,则肯定可以。有物品小于体积的一半,并且现在背包还没到一半,那么放进去肯定放得下,就直接放进去;如果此时超过了一半则同样可以。
代码5
#include<iostream>
using namespace std;
constexpr int MAXN = 2e5 + 5;
int main()
{
int t;
cin >> t;
while (t--)
{
int n;
//now是当前背包内的东西
long long w, now = 0;
cin >> n >> w;
//一半的大小,向上取整
long long half = (w + 1) / 2;
//标记是否能超过一半
bool b = false;
for (int i = 1; i <= n; i++)
{
//读入每一个物品
int temp;
cin >> temp;
//有物品超过一半就一直跳过
if (b)
{
continue;
}
//物品超过容量
if (temp > w)
{
continue;
}
//物品超过一半
if (temp >= half)
{
b = true;
}
//物品小于一半,直接放包里
else
{
now += temp;
//判断是否超过一半
if (now >= half)
{
b = true;
}
}
}
cout << (b ? "YES" : "NO") << endl;
}
return 0;
}
第六题 三回文序列
题目6
三回文序列是形如
a
…
a
⏟
k
1
b
…
b
⏟
k
2
a
…
a
⏟
k
1
\underbrace{a \dots a}_{k_1}\underbrace{b \dots b}_{k_2}\underbrace{a \dots a}_{k_1}
k1
a…ak2
b…bk1
a…a 的序列。
找到序列中最长的三回文的子序列的长度。
思路6
遍历
a
a
a 和
k
1
k_1
k1 可能的所有取值,然后从序列两端开始分别找
k
1
k_1
k1 个
a
a
a,然后再找剩下的部分中出现次数最多的
b
b
b。
找出现次数最多依然可以使用前缀和来加快运算速度。
代码6
#include<iostream>
#include<cstring>
using namespace std;
constexpr int MAXN = 2e5 + 5, MAXA = 26;
int nums[MAXN];
//前缀和
int sums[MAXN][MAXA + 1];
int main()
{
int t;
cin >> t;
while (t--)
{
int n, ans = 0;
cin >> n;
//清空前缀和
memset(sums, 0,sizeof(sums));
for (int i = 1; i <= n; i++)
{
cin >> nums[i];
}
//计算前缀和
for (int i = 1; i <= n; i++)
{
//遍历b的取值 1 - 26
for (int j = 1; j <= MAXA; j++)
{
if (nums[i] == j)
{
sums[i][j] = sums[i - 1][j] + 1;
}
else
{
sums[i][j] = sums[i - 1][j];
}
}
}
//遍历a的取值 1 - 26
for (int i = 1; i <= MAXA; i++)
{
//左侧指针,右侧指针,左侧a的次数,右侧a的次数
int pl = 1, pr = n, cntl = 0, cntr = 0;
//遍历k1 0 - n
for (int j = 0; j <= n; j++)
{
//移动左指针,找到j个a
while (pl < pr && cntl < j)
{
if (nums[pl] == i)
{
cntl++;
}
pl++;
}
//移动右指针,找到j个a
while (pl < pr && cntr < j)
{
if (nums[pr] == i)
{
cntr++;
}
pr--;
}
//找不到指定个数的a(j太大了)
if (cntl != j || cntr != j)
{
break;
}
//去找b的值
for (int k = 1; k <= MAXA; k++)
{
//更新答案,当前答案为j的个数乘以2(两边)加上中间出现的次数
ans = max(ans, 2 * j + sums[pr][k] - sums[pl - 1][k]);
}
}
}
cout << ans << endl;
}
return 0;
}
第七题 简单的异或问题
题目7
有一组整数 { 0 , 1 , 2 , … , 2 m − 1 } \lbrace 0, 1, 2, \dots , 2^m − 1 \rbrace {0,1,2,…,2m−1},请从中选出 k k k 个数,使得这 k k k 个数的异或和为 n n n, 请输出最大的满足条件的 k k k。
思路7
m
>
1
m > 1
m>1 时,
0
0
0 到
2
m
−
1
2^m - 1
2m−1 所有数异或得到的都是
0
0
0。所有的数可以组成
(
0
,
2
m
−
1
)
,
(
1
,
2
m
−
2
)
,
…
(0, 2^m - 1), (1, 2^m - 2), \dots
(0,2m−1),(1,2m−2),…的偶数组数,而每一组数的异或值都是
2
m
−
1
2^m - 1
2m−1,异或和也就是
0
0
0。
可以看出,每组数异或得到
2
m
−
1
2^m - 1
2m−1,所以用
2
m
−
1
2^m - 1
2m−1 和与
n
n
n 同组的数异或得到的数就是
n
n
n。而剩下的所有组有偶数个(
2
m
−
1
2 ^ m - 1
2m−1 和
n
n
n 的组不包括),异或和为
0
0
0,所以剩下的组都可以选择。最好的情况就是选了剩下组、
0
0
0、
2
m
−
1
2^m - 1
2m−1 和与
n
n
n 同组的数,一共
2
m
−
1
2 ^ m - 1
2m−1 个数。
但是
m
=
1
m = 1
m=1时不同,如果
n
=
0
n = 0
n=0,则只能选
0
0
0;如果
n
=
1
n = 1
n=1,则只能选
0
0
0 和
1
1
1 或
1
1
1。
此外还有两个特殊情况。
n
=
0
n = 0
n=0,此时可以把所有数都选上。
n
=
2
m
−
1
n = 2 ^ m - 1
n=2m−1,此时只不选
2
m
−
1
2 ^ m - 1
2m−1,也是
2
m
−
1
2 ^ m - 1
2m−1 个。
代码7
#include<iostream>
using namespace std;
int main()
{
long long n, m;
cin >> n >> m;
long long maxn = (1ll << m) - 1;
//n = 0, m > 1
if (n == 0 && m != 1)
{
cout << maxn + 1 << endl;
}
//m = 1
else if (m == 1)
{
if (n == 0)
{
cout << 1 << endl;
}
else
{
cout << 2 << endl;
}
}
//2 ^ m - 1的情况
else
{
cout << maxn << endl;
}
return 0;
}
第八题 子串的循环挪动
题目8
给出一个字符串
s
s
s,你需要执行
m
m
m 个任务。每个任务给出两个下标
l
i
l_i
li,
r
i
r_i
ri 和一个整数
k
i
k_i
ki(字符串的下标从
1
1
1 开始),表示你需要循环挪动
s
s
s 的子串
s
[
l
i
…
r
i
]
s[l_i \dots r_i]
s[li…ri]
k
i
k_i
ki 次。请从前到后依次执行给出的每个任务。
字符串的循环挪动操作:将最后一个字符移到第一个字符的位置,并且将其他所有字符向右移一个位置。
思路8
每次任务会将一个字符串分成四份:
- l i l_i li 之前的部分
- l i l_i li 到 子串中第 k k k 个之前的部分
- 子串中第 k k k 个到 l r l_r lr 的部分
- l r l_r lr 之后的部分
执行已至此之后字符串会重组成 1-3-2-4 的形式。
此外
1
≤
k
i
≤
1000000
1 \leq k_i \leq 1000000
1≤ki≤1000000,
k
k
k可能超过子串长度,需要先取模。
代码8
#include<iostream>
using namespace std;
int main()
{
string s;
cin >> s;
int m;
cin >> m;
while (m--)
{
int l, r, k;
cin >> l >> r >> k;
//取模
k %= (r - l + 1);
if (k == 0)
{
continue;
}
//重组
s = s.substr(0, l - 1) + s.substr(r - k, k) + s.substr(l - 1, r - l - k + 1) + s.substr(r, s.size() - r);
}
cout << s << endl;
return 0;
}
第九题 弗拉德和糖果 II
题目9
有
n
n
n 种糖果,第
i
i
i 种糖果有
a
i
a_i
ai 个(
1
≤
i
≤
n
1 \leq i \leq n
1≤i≤n)。
是否可以在不连续吃两个相同的糖果的情况下吃掉所有的糖果。
思路9
主要问题集中在最多的糖果上,因为最多的糖果最容易连续吃到。
如果最多的糖果要多于其他糖果的和再加一,则不管怎么吃,都会出现连续吃两次的情况。
如果小于等于,则可以保证每次吃一个最多的糖果,都有另一种糖果能吃。
代码9
#include<cstdio>
#include<algorithm>
constexpr int MAXN = 5000000 + 5;
long long a[MAXN];
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
{
scanf("%lld", a + i);
}
//排序找出最多的糖果
std::sort(a + 1, a + 1 + n);
//求和
long long sum = 0;
for (int i = 1; i < n; i++)
{
sum += a[i];
}
//判断条件
printf(a[n] <= sum + 1 ? "YES" : "NO");
return 0;
}
第十题 上帝的集合
题目10
现在上帝有一个空集合,现在他命令你为他执行下列三种操作 n n n 次,他每次会给你一个操作类型 o p op op。
- 向集合中插入一个整数 x x x;
- 将集合中所有的数加上 x x x;
- 输出集合中最小的数,并从集合中将他删除,如果存在多个最小的整数,任意选择一个即可;
思路10
这道题需要保持集合有序,所以可以使用优先队列存储数据。
操作
2
2
2 需要对所有数据进行操作,可能会花费很多时间。可以先存储所有加上的数的和,在出列的时候再加,入列的时候再减。
代码10
#include<vector>
#include<cstdio>
#include<queue>
using namespace std;
int main()
{
//优先队列,小的数优先
priority_queue<long long, vector<long long>, greater<long long> > q;
//o存储一共加了多少
long long n, o = 0;
scanf("%lld", &n);
while (n--)
{
int op;
long long x;
scanf("%d", &op);
switch (op)
{
case 1:
scanf("%lld", &x);
q.push(x - o);
break;
case 2:
scanf("%lld", &x);
o += x;
break;
case 3:
printf("%lld\n", q.top() + o);
q.pop();
break;
default:
break;
}
}
return 0;
}