纪念第一次非严格意义AK
Div.2题这么少,这不得冲一手
A. Robot Cleaner
题意:
一个
N
∗
M
N*M
N∗M 的矩阵,给出机器人的初始位置
(
r
b
,
c
b
)
(rb,cb)
(rb,cb) 以及目标点的位置
(
r
d
,
c
d
)
(rd,cd)
(rd,cd),初始机器人的运动方向
(
d
r
,
d
c
)
(dr,dc)
(dr,dc) 为
(
1
,
1
)
(1,1)
(1,1),如果碰到矩阵的边缘会自动掉头。
每次机器人都会清理所在行和列的所有点,问几步之后机器人第一次清理目标点。
思路:
看这个数据范围,别想啥算法了,直接上暴力。
代码:
时间复杂度: O ( n ∗ m ) O(n*m) O(n∗m)
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define re register int
#define PII pair<int,int>
#define x first
#define y second
#define cf int _; cin>> _; while(_--)
#define sf(x) scanf("%lld",&x)
#define sf2(x,y) scanf("%lld %lld",&x,&y)
#define pft(x) printf("%lld ",x)
#define pfn(x) printf("%lld\n",x)
#define fer(i,a,b) for(re i = a ; i <= b ; ++ i)
#define all(x) (x).begin(),(x).end()
const int N = 1e5 + 10, M = 350, mod = 1e9 + 7;
int n, m;
PII a, b;
signed main() {
cf{
sf2(n, m);
sf2(a.x, a.y);
sf2(b.x, b.y);
int res = 0, i = 1, j = 1;
while (a.x != b.x && a. y != b. y) {
if (a.x + i > n || a.x + i < 1)
i *= -1;
if (a.y + j > m || a.y + j < 1)
j *= -1;
a.x += i;
a.y += j;
res++;
}
pfn(res);
}
return 0;
}
B. Game on Ranges
题意:
有一个不相交的整数范围集合
S
S
S,最初只包含一个范围
[
1
,
n
]
[1,n]
[1,n]。每次从集合
S
S
S 中选取一个范围
[
l
,
r
]
[l,r]
[l,r],然后让在这个范围内选取一个数字
d
(
l
≤
d
≤
r
)
d (l≤d≤r)
d(l≤d≤r)并将其从集合
S
S
S。从
S
S
S 中移除
[
l
,
r
]
[l,r]
[l,r],并将范围
[
l
,
d
−
1
]
[l,d−1]
[l,d−1](如果
l
≤
d
−
1
l≤d−1
l≤d−1)和范围
[
d
+
1
,
r
]
[d+1,r]
[d+1,r](如果
d
+
1
≤
r
d+1≤r
d+1≤r)放入集合
S
S
S 中。当集合
S
S
S 为空时游戏结束。可以证明操作次数正好是
n
n
n。
问对于每一个
[
l
,
r
]
[l,r]
[l,r],找出其对应的
d
d
d。
思路:
确实,这道题有更简便的做法,但是看这数据范围,真的没必要想那么多。
我们不妨把这一个过程反着来看看,如果将之后用过的操作数删去,
[
l
,
r
]
[l,r]
[l,r] 中只会剩余一个数
d
d
d 没有被用过(这不废话吗),然后当一个区间的长度为
1
1
1 时,即
l
=
=
r
l==r
l==r,有且仅有一个数没有被操作过,那么我们可以每次都找出来区间中只有一个数没有被操作过的区间,然后在确定这个区间对应的
d
d
d 的同时将这个数标记为已经使用过。然后一步一步做下去就好了。
我的代码中有很多可以优化的地方,比如区间和可以用树状数组,或者说更好的做法什么的,但是时间很宽松的,随便浪吧。
代码:
时间复杂度: O ( n 2 ) O(n^{2}) O(n2)
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define re register int
#define PII pair<int,int>
#define x first
#define y second
#define cf int _; cin>> _; while(_--)
#define sf(x) scanf("%lld",&x)
#define sf2(x,y) scanf("%lld %lld",&x,&y)
#define pft(x) printf("%lld ",x)
#define pfn(x) printf("%lld\n",x)
#define fer(i,a,b) for(re i = a ; i <= b ; ++ i)
#define all(x) (x).begin(),(x).end()
const int N = 1e5 + 10, M = 350, mod = 1e9 + 7;
int n;
int st[1010];
int get(int a, int b) {
int res = 0;
for (int i = a; i <= b; i++)
if (!st[i])
res++;
return res;
}
pair<PII, int> s[1010];
signed main() {
cf{
sf(n);
queue<int> q;
for (int i = 0; i < n; i++) {
sf2(s[i].x.x, s[i].x.y);
s[i].y = 0;
q.push(i);
}
memset(st, 0, sizeof st);
while (!q.empty()) {
int t = q.front();
q.pop();
if (get(s[t].x.x, s[t].x.y) != 1) {
q.push(t);
continue;
}
int j = 0;
for (int i = s[t].x.x; i <= s[t].x.y; i++)
if (!st[i]) {
j = i;
break;
}
s[t].y = j;
st[j] = true;
}
for (int i = 0; i < n; i++)
cout << s[i].x.x << ' ' << s[i].x.y << ' ' << s[i].y << '\n';
}
return 0;
}
C. Balanced Stone Heaps
题意:
有
n
n
n 堆石头,每堆有
s
[
i
]
s[i]
s[i] 个石头。接下来从第
3
3
3 堆遍历到第
n
n
n 堆,依次进行一次以下操作:
设
i
i
i 是当前堆的编号。任选数字
d
(
0
≤
3
∗
d
≤
s
[
i
]
)
d(0≤3*d≤s[i])
d(0≤3∗d≤s[i]),将
d
d
d 个石头从第
i
i
i 堆移至第
(
i
−
1
)
(i−1)
(i−1) 堆,将
2
∗
d
2*d
2∗d 个石头从第
i
i
i 堆移至第
(
i
−
2
)
(i−2)
(i−2) 堆。因此,在
s
[
i
]
s[i]
s[i] 降低
3
∗
d
3*d
3∗d 后,
s
[
i
−
1
]
s[i-1]
s[i−1] 增加
d
d
d,
s
[
i
−
2
]
s[i-2]
s[i−2] 增加
2
∗
d
2*d
2∗d。
遍历之后,问
s
[
i
]
s[i]
s[i] 最小的堆中最大的石头数量是多少?
思路:
一看问题,最小值最大,那么二分的概率是很大了。
那么二分出答案
m
i
d
mid
mid 之后,怎么判断它是否合法呢?这里需要用到一个从后往前的贪心(如果从前往后的话是无法确定最优的),即如果原始的
s
[
i
]
+
(
i
+
1
)
、
(
i
+
2
)
s[i]+(i+1)、(i+2)
s[i]+(i+1)、(i+2)转移过来的总和
d
[
i
]
d[i]
d[i]
≥
m
i
d
\geq mid
≥mid,那么第
i
i
i 个点是可以满足的,同时将对应的
d
=
⌊
m
i
n
(
s
[
i
]
,
d
[
i
]
+
s
[
i
]
−
m
i
d
)
3
⌋
d=\lfloor \frac{min(s[i],d[i]+s[i]-mid)}{3} \rfloor
d=⌊3min(s[i],d[i]+s[i]−mid)⌋,因为要保证前面的值尽可能的大。
代码:
时间复杂度: O ( n ∗ log n ) O(n*\log{n}) O(n∗logn)
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define re register int
#define PII pair<int,int>
#define x first
#define y second
#define cf int _; cin>> _; while(_--)
#define sf(x) scanf("%lld",&x)
#define sf2(x,y) scanf("%lld %lld",&x,&y)
#define pft(x) printf("%lld ",x)
#define pfn(x) printf("%lld\n",x)
#define fer(i,a,b) for(re i = a ; i <= b ; ++ i)
#define all(x) (x).begin(),(x).end()
const int N = 2e5 + 10, M = 350, mod = 1e9 + 7;
int n;
int s[N];
int d[N], st[N];
bool check(int u) {
for (int i = 1; i <= n; i++)
d[i] = s[i], st[i] = 0;
for (int i = n; i >= 3; i--)
if (d[i] + st[i] >= u) {
int tmp = d[i] + st[i] - u;
tmp = min(tmp, d[i]);
tmp /= 3;
st[i - 1] += tmp;
st[i - 2] += tmp * 2;
} else
return 0;
if (d[2] + st[2] < u)
return 0;
if (d[1] + st[1] < u)
return 0;
return 1;
}
signed main() {
cf{
sf(n);
for (int i = 1; i <= n; i++)
sf(s[i]);
int l = 1, r = 1e9 + 10;
while (l != r) {
int mid = l + r + 1 >> 1;
if (check(mid))
l = mid;
else
r = mid - 1;
}
pfn(l);
}
return 0;
}
D. Robot Cleaner Revisit
题意:
题意和 A A A 题差不多,只不过机器人从“必定会清理所在行和列上的所有点”变为了“有 p 100 \frac{p}{100} 100p 的概率会清理所在行和列上的所有点”。
思路:
首先我们先明确一点,机器人的运动轨迹是无限长,但是是存在循环结的。
那么我们要找的答案,就是循环结中每个符合条件的点的期望步数再求和,由期望的线性性质可知期望是可加的。
那么对于任意一个符合条件的点来说,它的期望步数又是怎么求的呢?设循环结的长度是 l e n len len,其中有 k k k 个符合条件的点;走到第 i ( 0 ≤ i ≤ k − 1 ) i(0\leq i\leq k-1) i(0≤i≤k−1) 个点能够清理掉目标点的期望步数为 E i E_{i} Ei,清理成功的概率是 p p p,失败的概率是 m p = 1 − p mp=1-p mp=1−p。那么我们能够得到一个式子: E i = ∑ j ≥ 0 ( s t e p [ i ] + j ∗ l e n ) ∗ m p j ∗ k + i ∗ p E_{i}=\sum_{j\geq 0}{(step[i]+j*len)*mp^{j*k+i}*p} Ei=j≥0∑(step[i]+j∗len)∗mpj∗k+i∗p = p ∗ s t e p [ i ] ∗ m p i 1 − m p k + m p k ∗ l e n ∗ p ∗ m p i ( 1 − m p k ) 2 =\frac{p*step[i]*mp^{i}}{1-mp^{k}}+\frac{mp^{k}*len*p*mp^{i}}{(1-mp^{k})^{2}} =1−mpkp∗step[i]∗mpi+(1−mpk)2mpk∗len∗p∗mpi所以总的期望为: E = ∑ i = 0 k − 1 E i = p ∗ ∑ i = 0 k − 1 s t e p [ i ] ∗ m p i + l e n ∗ m p k 1 − m p k E=\sum_{i=0}^{k-1}{E_{i}}=\frac{p*\sum_{i=0}^{k-1}{step[i]*mp^{i}}+len*mp^{k}}{1-mp^{k}} E=i=0∑k−1Ei=1−mpkp∗∑i=0k−1step[i]∗mpi+len∗mpk
代码:
时间复杂度: O ( 2 ∗ n ∗ m ) O(2*n*m) O(2∗n∗m)
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define re register int
#define PII pair<int,int>
#define x first
#define y second
#define cf int _; cin>> _; while(_--)
#define sf(x) scanf("%lld",&x)
#define sf2(x,y) scanf("%lld %lld",&x,&y)
#define pft(x) printf("%lld ",x)
#define pfn(x) printf("%lld\n",x)
#define fer(i,a,b) for(re i = a ; i <= b ; ++ i)
#define all(x) (x).begin(),(x).end()
const int N = 1e5 + 10, M = 350, mod = 1e9 + 7;
inline int qpow(int a, int b, int c) {
int res = 1 % c;
a %= c;
while (b > 0) {
if (b & 1)
res = res * a % c;
a = a * a % c;
b >>= 1;
}
return res;
}
inline int inv(int u) {
return qpow(u, mod - 2ll, mod);
}
int rb, rd, cb, cd;
int n, m, p;
signed main() {
cf{
sf2(n, m), sf2(rb, cb), sf2(rd, cd), sf(p);
p = p * inv(100) % mod;
int pre = 1;
int idx = 0, ans = 0;
int dr = 1, dc = 1;
map<array<int, 4>, bool> q;
while (true) {
if (rb + dr < 1 || rb + dr > n)
dr *= -1;
if (cb + dc < 1 || cb + dc > m)
dc *= -1;
if (q[ {rb, cb, dr, dc}])
break;
q[ {rb, cb, dr, dc}] = true;
if (rb == rd || cb == cd) {
ans = (ans + idx * pre % mod * p % mod) % mod;
pre = pre * (1 - p + mod) % mod;
}
rb += dr, cb += dc;
idx++;
}
ans = (ans + idx * pre % mod) % mod;
ans = ans * inv((1 - pre + mod) % mod) % mod;
pfn(ans);
}
return 0;
}
补充
dongdziz哥真的太强啦Orz,佬用了完全不同的一种化简方式,具体过程我不太会用语言描述,反正就是推数学公式,然后找规律,运用等比数列求和以及错位相减等方法将其简化为一个数学方程式……具体看代码(公式推导):
ps:在找循环节时有很多种不同的方法,很明显这种方式更加的高效
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define re register int
#define PII pair<int,int>
#define x first
#define y second
#define cf int _; cin>> _; while(_--)
#define sf(x) scanf("%lld",&x)
#define sf2(x,y) scanf("%lld %lld",&x,&y)
#define pft(x) printf("%lld ",x)
#define pfn(x) printf("%lld\n",x)
#define fer(i,a,b) for(re i = a ; i <= b ; ++ i)
#define all(x) (x).begin(),(x).end()
const int N = 1e5 + 10, M = 350, mod = 1e9 + 7;
inline int qpow(int a, int b, int c) {
int res = 1 % c;
a %= c;
while (b > 0) {
if (b & 1)
res = res * a % c;
a = a * a % c;
b >>= 1;
}
return res;
}
inline int inv(int u) {
return qpow(u, mod - 2ll, mod);
}
int rb, rd, cb, cd;
int n, m, p, q;
signed main() {
cf {
sf2(n, m), sf2(rb, cb), sf2(rd, cd), sf(p);
p = (p * inv(100)) % mod;
q = (((1 - p) % mod) + mod) % mod;
int ans = 0, end = 2 * (n - 1) * (m - 1);
int c1 = mod+10, c2 = mod+10;
if (rb == rd)
c1 = 0, c2 = (n - rb) * 2;
else if (rb > rd)
c1 = 2 * n - rb - rd, c2 = c1 + 2 * (rd - 1);
else
c1 = rd - rb, c2 = c1 + 2 * (n - rd);
vector<int> ve;
for (int i = 0; i < end; i += 2 * (n - 1)) {
if (i + c1 < end)
ve.push_back(i + c1);
if (i + c2 < end)
ve.push_back(i + c2);
}
c1 = mod+10, c2 = mod+10;
if (cb == cd)
c1 = 0, c2 = (m - cb) * 2;
else if (cb > cd)
c1 = 2 * m - cb - cd, c2 = c1 + 2 * (cd - 1);
else
c1 = cd - cb, c2 = c1 + 2 * (m - cd);
for (int i = 0; i < end; i += 2 * (m - 1)) {
if (i + c1 < end)
ve.push_back(i + c1);
if (i + c2 < end)
ve.push_back(i + c2);
}
sort(ve.begin(), ve.end());
ve.erase(unique(ve.begin(), ve.end()), ve.end());
int k = ve.size();
int pp = p;
int tp = qpow(1 - p, k, mod);
int inp = inv(1 - tp);
for (int i = 0; i < ve.size(); i++) {
int now = ve[i];
ans = (ans + pp * (now * inp % mod + end * tp % mod * inp % mod * inp % mod) % mod) % mod;
pp = pp * (1 - p) % mod;
}
ans = (ans % mod + mod) % mod;
pfn(ans);
}
}
E. Middle Duplication
题意:
有一棵二叉树,每个节点上都有一个小写字母,定义一颗树的价值就是其中序遍历后得到的字符串。
现在有一种操作,对于每个节点,我们最多可以复制它的标签一次,即
′
e
′
−
>
′
e
e
′
'e'->'ee'
′e′−>′ee′,但是这种操作是有条件的,前提是当前节点是树的根,或者其父节点的字母也被复制。
给定树和一个整数k,最多可以复制k个节点的字母,求这棵二叉树所能得到的最小价值(中序遍历的字典序最小)。
思路:
在比赛时我一看这道题,脑子里就想到了两个知识点,一个是二叉树的中序遍历,也就是题目中价值的求法;其次就是二叉搜索树的删除节点操作,就是找到一个节点左子树的最右节点,或者是右子树的最左节点。但是最终还是由于码力不够未能如愿。
首先我们先确定什么样的节点复制之后能够使树的价值变小,很明显,这个字母在中序遍历中,从当前字母依次往后找到第一个与其不同的字母,如果本字母小于后面那个不同的字母,那个当前这个字母是值得被复制的。
那么做法就已经清晰的不能再清晰,将中序遍历得到的字符串从头开始遍历,如果这个字母值得被复制,那么就判断剩余的操作次数是否够,如果够的话就将根节点到本节点路径上所有节点都复制并将操作次数减去,否则的话就判断下一个节点。
这其中需要很多很多的判断条件,详细说明放到代码注释中吧。
代码:
时间复杂度: O ( n ∗ log n ) O(n*\log{n}) O(n∗logn)
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define re register int
#define PII pair<int,int>
#define x first
#define y second
#define cf int _; cin>> _; while(_--)
#define sf(x) scanf("%lld",&x)
#define sf2(x,y) scanf("%lld %lld",&x,&y)
#define pft(x) printf("%lld ",x)
#define pfn(x) printf("%lld\n",x)
#define fer(i,a,b) for(re i = a ; i <= b ; ++ i)
#define all(x) (x).begin(),(x).end()
const int N = 2e5 + 10, M = 350, mod = 1e9 + 7;
int n, k;
char s[N];//每个节点对应的字符
int r[N], l[N];//每个节点的左右儿子下标
int mid[N], idx;//得到的中序遍历,mid中存储的是对应位置节点的下标
int ne[N];//每个节点中序遍历中后面第一个与之不同的节点的下标
bool st[N];//标记节点是否被复制
void find_mid(int u) {//得到初始的中序遍历结果
if (!u)
return ;
find_mid(l[u]);
mid[++idx] = u;
find_mid(r[u]);
}
void dfs(int u, int ans) {//u为当前节点,ans为复制u节点所需要的操作数
if (!u || !k)
return ;
//如果说当前节点不存在或者操作数为零都不能进行操作
//整体结构和中序遍历类似
dfs(l[u], ans + 1);
if (l[u] && st[l[u]]) {
st[u] = true;
ans = 0;
}//如果做儿子存在且值得且已经被复制,那么u节点复制的代价就已经被计算了
if (ans > k)
return ;
if (s[u] < s[ne[u]]) {
st[u] = true;
k -= ans;
ans = 0;
}//判断当前节点是否值得复制(就算已经被标记过也没关系,ans==0)
if (st[u]) {
dfs(r[u], ans + 1);
if (r[u] && st[r[u]])
st[u] = true;
}//只有当前节点已经被复制过,它的右子树中的节点才能考虑是否可以复制
//如果u节点不值得被复制,那么只要右子树中的节点复制,u节点必然被复制,则字典序必然增大
}
void get(int u) {//再次中序遍历,目的是输出答案
if (!u)
return ;
get(l[u]);
printf("%c", s[u]);
if (st[u])
printf("%c", s[u]);
get(r[u]);
}
signed main() {
sf2(n, k);
cin >> s + 1;
for (int i = 1; i <= n; i++)
sf2(l[i], r[i]);
find_mid(1);//读题,题目已经给出1为根节点下标
ne[mid[n]] = mid[n];
//最后一个点是必然不可以的,复制一定会使字典序变大
//因此这里将其后缀复制为本身,之后判断是否小于后缀即可
for (int i = n - 1; i >= 1; i--)
if (s[mid[i]] != s[mid[i + 1]])
ne[mid[i]] = mid[i + 1];
else
ne[mid[i]] = ne[mid[i + 1]];
//中序遍历从后往前找当前节点之后第一个不同的节点的下标
dfs(1, 1);
get(1);
return 0;
}