Round #804 (Div. 2) D(DP)
Educational Round 131 D(greedy)
题意
对于一个长为 n 的数组 b,求一个 1-n 的排列 a 使得, b i = f l o o r ( i a i ) b_i=floor(\frac{i}{a_i}) bi=floor(aii) .
思路
通过
b
i
b_i
bi 我们可以求出每个位置的取值区间:
(1)当
b
i
=
0
,
i
+
1
≤
a
i
≤
n
b_i=0, i+1{\leq}a_i{\leq}n
bi=0,i+1≤ai≤n
(2)当
b
i
>
0
,
i
b
i
+
1
+
1
≤
a
i
≤
i
b
i
b_i>0,\frac{i}{b_i+1}+1{\leq}a_i{\leq}\frac{i}{b_i}
bi>0,bi+1i+1≤ai≤bii
然后我就开始快乐地dfs
实际上,知道每个位置的取值区间,问题可以抽象为位置上放点,和具体数字无关。枚举点(数字) i,显然,约束区间
[
L
,
R
]
且
L
≤
i
[L,R]且L\leq{i}
[L,R]且L≤i 的位置可以容纳该点,而我们为了保证所有的点可以放,要贪心地选择 R 尽量小的位置。
代码
const int N = 500050;
int b[N], ans[N];
int n;
vector<pair<int, int> > g[N];
priority_queue<pair<int, int>, vector<pair<int, int> >, greater<pair<int, int> > > pq;
void solve() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> b[i], g[i].clear();
for (int i = 1; i <= n; i++) {
int L = i / (b[i] + 1) + 1;
int R = (b[i] == 0) ? n : (i / b[i]);
g[L].push_back({R, i});
}
for (int i = 1; i <= n; i++) {
for (auto j : g[i]) pq.push(j);
auto p = pq.top(); pq.pop();
ans[p.second] = i;
}
for (int i = 1; i <= n; i++) cout << ans[i] << ' ';
cout << '\n';
}
其他区间相关问题
参考《算法竞赛入门经典》 刘汝佳
- 选择不相交区间:有 n 个区间 ( a i , b i ) (a_i,b_i) (ai,bi) ,选择尽量多的区间,使得两两不相交。
- 区间选点问题:有 n 个区间 [ a i , b i ] [a_i,b_i] [ai,bi] ,选择尽量少的点,使得每个区间都至少包含一个点。
- 区间覆盖问题:有 n 个区间 [ a i , b i ] [a_i,b_i] [ai,bi] ,选择尽量少的区间覆盖一条指定线段 [ s , t ] [s,t] [s,t] 。
Global Round 19 E(brute force)
题意
对于一个数组,设元素 x 在其中出现了
c
n
t
x
cnt_x
cntx 次。S是一些禁止选择的二元组形如
(
x
,
y
)
(x,y)
(x,y) ,表示不能取
(
x
,
y
)
(
y
,
x
)
(x,y)(y,x)
(x,y)(y,x) ,求
m
a
x
(
(
c
n
t
x
+
c
n
t
y
)
∗
(
x
+
y
)
)
(
x
≠
y
)
max((cnt_x+cnt_y)*(x+y))(x\neq{y})
max((cntx+cnty)∗(x+y))(x=y)
思路
参考:官方题解
对于一个 x ,我们遍历所有满足
c
n
t
y
≤
c
n
t
x
cnt_y\leq{cnt_x}
cnty≤cntx 的 y ,操作复杂度为
O
(
∑
c
n
t
x
)
=
O
(
n
)
O(\sum{cnt_x})=O(n)
O(∑cntx)=O(n) ,通过 set
l
o
g
m
logm
logm 判断是否bad,更新答案。
代码
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
void solve() {
int n, m;
cin >> n >> m;
vector<int> a(n);
map<int, int> cnt;
for (int i = 0; i < n; i++) {
cin >> a[i];
cnt[a[i]]++;
}
set<pair<int, int> > bad;
for (int i = 0; i < m; i++) {
int x, y;
cin >> x >> y;
bad.insert({x, y}); // minmax(x, y);
bad.insert({y, x});
}
vector<vector<int> > occ(n);
for (auto &i : cnt) {
occ[i.second].push_back(i.first);
}
for (auto &v : occ) {
reverse(v.begin(), v.end()); // 降序
}
ll ans = 0;
for (int cnt_x = 1; cnt_x < n; cnt_x++) {
for (int x : occ[cnt_x]) {
for (int cnt_y = 1; cnt_y <= cnt_x; cnt_y++) {
for (int y : occ[cnt_y]) {
if (x != y && bad.find({x, y}) == bad.end()) {
ans = max(ans, 1ll * (cnt_x + cnt_y) * (x + y));
break;
}
}
}
}
}
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t; cin >> t;
while (t--)
solve();
return 0;
}
Round #805 (Div. 3) G2(LCA)
题意
给定一棵 n 个节点的无向无根树,要求回答 q 个询问,每个询问给出 k 个顶点,问是否存在一条简单路径经过这 k 个点。
其中
1
≤
n
≤
2
e
5
,
∑
i
k
i
≤
2
e
5
1\leq{n}\leq{2e5},\sum_ik_i\leq2e5
1≤n≤2e5,∑iki≤2e5
思路
无根树上的一条最短路径,可以转化成有根树上一条或两条末尾元素相等的链。G1 在询问数较小时,我们可以通过 dp
O
(
n
)
O(n)
O(n) 判断。其实在同一条链的关系,我们可以通过祖先关系判断,如果两个点存在祖先关系则它们在同一条链上。我们将这 k 个点按深度排序,尝试将这些点放在至多两条链上,而且放在两条链上时要保证末尾元素相等,且该元素深度最小。
祖先关系的判断借助LCA实现。
回顾一下倍增LCA:
f
[
u
]
[
i
]
f[u][i]
f[u][i] :代表节点 u 往上跳
2
i
2^i
2i 步所达节点,
f
[
u
]
[
0
]
f[u][0]
f[u][0] 为 u 的父亲
显然对于一棵有 n 个节点的树,最多可以跳
⌊
l
o
g
2
(
n
)
⌋
\lfloor{log}_2(n)\rfloor
⌊log2(n)⌋ 步。
我们通过 dfs 求 f 数组,求节点 u 时,它的祖先节点已经求了 f,有递推公式:
f
[
u
]
[
i
+
1
]
=
f
[
f
[
u
]
[
i
]
]
[
i
]
f[u][i + 1] = f[f[u][i]][i]
f[u][i+1]=f[f[u][i]][i] .
求节点 x 和 y 的 LCA 时,首先我们将 x,y 跳至同一深度(深度较大的单独跳跃),之后一起跳至 LCA 的儿子节点。
O
(
l
o
g
n
)
O(logn)
O(logn) 实现对 LCA 的求解。
代码
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e5 + 5;
int n;
vector<int> g[N];
int dep[N], f[N][30];
int mx;
void ini()
{
mx = (int)(log(n) / log(2));
memset(dep, 0, sizeof dep);
memset(f, -1, sizeof f);
for (int i = 0; i <= n; i++)
g[i].clear();
}
void dfs(int u, int fa)
{
dep[u] = dep[fa] + 1;
for (int i = 0; i <= mx; i++)
if (f[u][i] != -1)
f[u][i + 1] = f[f[u][i]][i];
for (auto i : g[u])
{
if (i == fa)
continue;
f[i][0] = u;
dfs(i, u);
}
}
int LCA(int x, int y)
{
if (dep[x] < dep[y]) swap(x, y);
for (int i = mx; i >= 0; i--)
{
if (f[x][i] != -1 && dep[f[x][i]] >= dep[y])
{
x = f[x][i];
}
if (x == y) return x;
}
for (int i = mx; i >= 0; i--)
{
if (f[x][i] != f[y][i])
{
x = f[x][i];
y = f[y][i];
}
}
return f[x][0];
}
void solve() {
cin >> n;
ini();
int x, y;
for (int i = 1; i < n; i++) {
cin >> x >> y;
g[x].push_back(y);
g[y].push_back(x);
}
dfs(1, 0);
vector<int> p(n);
int q; cin >> q;
for (int i = 1; i <= q; i++) {
int k; cin >> k;
for (int j = 0; j < k; j++) {
cin >> p[j];
}
sort(p.begin(), p.begin() + k, [&](int x, int y) -> bool { return dep[x] > dep[y]; });
bool flag = 1;
int L1 = p[0], L2 = -1, L = -1;
for (int j = 1; j < k; j++) {
if (L2 == -1) {
if (LCA(L1, p[j]) != p[j]) {
L2 = p[j];
L = LCA(p[0], p[j]);
if (L != -1 && dep[p[k - 1]] < dep[L]) {
flag = 0;
break;
}
}
else L1 = p[j];
}
else {
if (LCA(L1, p[j]) == p[j]) {
L1 = p[j];
continue;
}
if (LCA(L2, p[j]) == p[j]) {
L2 = p[j];
continue;
}
flag = 0;
break;
}
}
if (flag) cout << "YES\n";
else cout << "NO\n";
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
solve();
return 0;
}
Codeforces Round #811 (Div. 3) D(string、DP)
题意
给定一个字符串 s ,以及 n 个匹配串 t i t_i ti,如果匹配串 t i t_i ti 匹配 s 的某一子串,则可以将该区间染色。求能否将 s 完全染色并且输出最小染色次数和对应的方案。
思路
DP:设状态
f
(
i
)
f(i)
f(i) 为匹配了 s 的前 i 个字符所需的最小染色次数,显然,答案为
f
(
l
e
n
(
s
)
)
f(len(s))
f(len(s)) .
状态转移:枚举 s 匹配结束位置 i ,枚举匹配串,看是否可以完全匹配,若可以我们可以进行转移:
f
(
i
)
=
m
i
n
i
−
l
e
n
(
j
)
+
1
<
=
l
<
=
i
{
f
(
i
)
,
f
(
l
−
1
)
}
f(i)=min_{i-len(j)+1<=l<=i}{\{f(i),f(l-1)\}}
f(i)=mini−len(j)+1<=l<=i{f(i),f(l−1)}
其中
l
e
n
(
j
)
len(j)
len(j) 为第 j 个匹配串的长度。
同时记录所使用的匹配串、匹配的起点位置、上一个状态等信息输出答案。
总体时间复杂度为
O
(
m
n
∗
l
e
n
g
t
h
(
t
)
)
O(mn*length(t))
O(mn∗length(t))
一开始看到直接蒙了,没怎么做过字符串的题,字符数组 char[] 的用法 mark 一下。
代码
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int maxn = 105;
char s[maxn], t[15][15];
int f[maxn], len[20];
int w[maxn], p[maxn], g[maxn];
void solve() {
int n, m;
cin >> s + 1 >> n;
m = strlen(s + 1);
for (int i = 1; i <= n; i++) {
cin >> t[i] + 1;
len[i] = strlen(t[i] + 1);
}
memset(f, 0x3f, sizeof f);
f[0] = 0;
for (int i = 1; i <= m; i++) { // 枚举主串位置i
for (int j = 1; j <= n; j++) { // 枚举子串
bool ok = 1; // 以s[i]为结束字符,当前子串是否可以匹配
for (int k = len[j], l = i; k; k--, l--) {
if (t[j][k] != s[l]) { ok = 0; break; }
}
if (!ok) continue;
// 若可以匹配,则f(i)=min(f(i),f(l-1)),i-len(j)+1<=l<=i
for (int k = len[j], l = i; k; k--, l--) {
if (f[l - 1] + 1 < f[i]) {
f[i] = f[l - 1] + 1;
w[i] = j, p[i] = i - len[j] + 1;
g[i] = l - 1;
}
}
}
}
if (f[m] > m) { cout << "-1\n"; return; }
cout << f[m] << '\n';
vector<pair<int, int> > ans;
for (int i = m; i; i = g[i]) {
ans.push_back({w[i], p[i]});
}
reverse(ans.begin(), ans.end());
for (auto [x, y] : ans) cout << x << ' ' << y << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t; cin >> t;
while (t--) solve();
return 0;
}
CodeTON Round 2 (Div. 1 + Div. 2) D(思维)
题意
思路
C 卡了好久,D 一开始看到只想到了奇偶性,这种
元素索引
∗
元素值
元素索引*元素值
元素索引∗元素值 的不变性质还是挺有意思的,也可以理解为前(后)缀和数组的和不变。
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
void solve() {
int n, m;
cin >> n >> m;
vector<ll> f(n, 0);
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
ll x; cin >> x;
f[i] += j * x;
}
}
map<ll, int> cnt;
for (int i = 0; i < n; i++) cnt[f[i]]++;
for (int i = 0; i < n; i++) {
if (cnt[f[i]] == 1) {
cout << i + 1 << ' ' << f[i] - f[(i + 1) % n] << '\n';
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t; cin >> t;
while (t--) solve();
return 0;
}
Educational Codeforces Round 134 D.Maximum AND(贪心)
题意
给定长度为 n 的序列 a 和 b,调整序列 b 元素的排序,使得 c [ i ] = ( a [ i ] ) x o r ( b [ i ] ) c[i]=(a[i])xor(b[i]) c[i]=(a[i])xor(b[i]) , c [ 1 ] & c [ 2 ] & . . . & c [ n ] c[1]\&c[2]\&...\&c[n] c[1]&c[2]&...&c[n] 取得最大值。
思路
比赛中,我想到了从高位往低位贪心的思路,但后面判断能不能找到一一对应的关系卡住了呜呜。
正解:假设式子可取值 x ,实际上给出对 x 二进制表示为 ‘1’ 的位置的约束,
a
[
i
]
a[i]
a[i] 一定存在在这些位置取反的
b
[
j
]
b[j]
b[j] ,公式表达
a
[
i
]
&
x
=
∼
b
[
j
]
&
x
a[i]\&x=\sim{}b[j]\&x
a[i]&x=∼b[j]&x ,且这种关系是一一对应的。实际上这仅是式子能取值 x 的一个必要条件,但由于最大值一定存在,式子一定能取到该最大值。
AC代码
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e5 + 10;
int a[N], b[N], c[N];
void solve() {
int n; cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) cin >> b[i];
auto check = [&](int x) {
vector<int> c(n), d(n);
for (int i = 0; i < n; i++) {
c[i] = a[i + 1] & x;
d[i] = ~b[i + 1] & x;
}
sort(c.begin(), c.end());
sort(d.begin(), d.end());
return c == d;
};
int ans = 0;
for (int d = 29; d >= 0; d--) {
if (check(ans | 1 << d)) {
ans |= 1 << d;
}
}
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t; cin >> t;
while (t--) solve();
return 0;
}
Educational Codeforces Round 133 C. Robot in a Hallway(DP)D. Chip Move(DP)
总之,edu场就是狠狠地被教育~~
C
题意
给出一个 2 ∗ m 2*m 2∗m 的平面,每个格子只有到一定时间才可以通过,问从 ( 1 , 1 ) (1,1) (1,1) 出发不重不漏地走一遍的最少时间。
思路
由于平面只有2行,要求不重不漏的话可能路径只有蛇形走位或者在某个时刻放弃蛇形走位直行再折返。
关键是维护直行再折返的时间,实际上从位置
(
i
,
j
)
(i,j)
(i,j) 直行至
(
i
,
n
)
(i,n)
(i,n) 的时间为
m
a
x
j
≤
k
≤
n
(
t
+
n
−
j
,
a
[
i
]
[
k
]
+
(
n
−
k
+
1
)
)
max_{j\leq{k}\leq{n}}(t+n-j,a[i][k]+(n-k+1))
maxj≤k≤n(t+n−j,a[i][k]+(n−k+1)) ,观察到
a
[
i
]
[
k
]
−
k
a[i][k]-k
a[i][k]−k 为变化量,我们维护后缀最值
v
1
j
=
m
a
x
j
≤
k
≤
n
(
a
[
i
]
[
k
]
−
k
)
)
v_{1j}=max_{j\leq{k}\leq{n}}(a[i][k]-k))
v1j=maxj≤k≤n(a[i][k]−k)) ;同样折返,从
(
i
x
o
r
1
,
n
)
(ixor1,n)
(ixor1,n) 折返直行至
(
i
,
j
)
(i,j)
(i,j) ,用时
m
a
x
j
≤
k
≤
n
(
t
+
n
−
y
+
1
,
a
[
i
]
[
k
]
+
(
k
−
j
+
1
)
)
max_{j\leq{k}\leq{n}}(t+n-y+1,a[i][k]+(k-j+1))
maxj≤k≤n(t+n−y+1,a[i][k]+(k−j+1)) ,维护后缀最值
v
2
j
=
m
a
x
j
≤
k
≤
n
(
a
[
i
]
[
k
]
+
k
)
)
v_{2j}=max_{j\leq{k}\leq{n}}(a[i][k]+k))
v2j=maxj≤k≤n(a[i][k]+k)) ,这样计算时可以
O
(
1
)
O(1)
O(1) 地算
m
a
x
(
t
+
n
−
y
+
1
,
v
2
j
−
j
+
1
)
max(t+n−y+1,v_{2j}-j+1)
max(t+n−y+1,v2j−j+1) 即可。
AC代码
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e5 + 5;
ll a[2][N], v1[2][N], v2[2][N];
bool v[2][N];
void solve() {
int n; cin >> n;
for (int i = 0; i < 2; i++) {
for (int j = 1; j <= n; j++) {
cin >> a[i][j], v[i][j] = false;
}
v1[i][n + 1] = v2[i][n + 1] = -1e18;
// 求a[i][j]+j和a[i][j]-j的后缀最值
for (int j = n; j ; j--) {
v1[i][j] = max(v1[i][j + 1], a[i][j] + j);
v2[i][j] = max(v2[i][j + 1], a[i][j] - j);
}
}
int x = 0, y = 1;
v[0][1] = true;
ll ans = 1e18, t = 0;
while(1) {
if (!v[x ^ 1][y]) {
// 直行并折回v[x^1][y]
ll tmp = max(t + n - y, n + 1 + v2[x][y + 1]);
ans = min(ans, max(tmp + n - y + 1, v1[x ^ 1][y] - y + 1));
x ^= 1;
}
else y++;
if (y > n) break;
// 移动至(x,y)
v[x][y] = true;
t = max(t + 1, a[x][y] + 1); // 蛇形走位
}
cout << min(t, ans) << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t; cin >> t;
while (t--) solve();
return 0;
}
D
题意
给定 n, k ,第 i 步可以向前跳 ( i + k − 1 ) (i+k-1) (i+k−1) 的倍数步,问从 0 到位置 1~n 的方案数。
思路
前 i 次至少跳
i
(
i
+
1
)
/
2
i(i+1)/2
i(i+1)/2 ,因此跳的次数是
O
(
n
)
O(\sqrt{n})
O(n) 的 。
设第
i
i
i 步到达位置
j
j
j 的方案数为
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] ,有递推公式
d
p
[
i
]
[
j
]
=
∑
d
p
[
i
−
1
]
[
j
−
(
k
+
i
)
∗
t
]
=
d
p
[
i
]
[
j
−
(
k
+
i
)
]
+
d
p
[
i
−
1
]
[
j
−
(
k
+
i
)
]
dp[i][j]=\sum{dp[i-1][j-(k+i)*t]} ={dp[i][j-(k+i)]}+dp[i-1][j-(k+i)]
dp[i][j]=∑dp[i−1][j−(k+i)∗t]=dp[i][j−(k+i)]+dp[i−1][j−(k+i)]
AC代码
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int mod = 998244353;
void solve() {
int n, k;
cin >> n >> k;
vector<int> ans(n + 1);
vector<int> dp(n + 1);
dp[0] = 1;
int sum = 0;
for (int x = k; ; x++) {
sum += x;
if (sum > n) break;
for (int i = n; i >= sum - x; i--) {
dp[i] = i >= sum ? dp[i - x] : 0; // 走一步到位置i的方案数
}
for (int i = sum; i <= n; i++) {
dp[i] += dp[i - x]; // 此步到达位置j的方案数
dp[i] %= mod;
}
for (int i = sum; i <= n; i++) {
ans[i] += dp[i];
ans[i] %= mod;
}
}
for (int i = 1; i <= n; i++) {
cout << ans[i] << " \n"[i == n];
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
solve();
return 0;
}