Codeforces Round #678
写在前面
Codeforces Round #678 (Div. 2)
比赛地址:https://codeforces.com/contest/1436。
脑子犯浑。
A
\(t\) 组数据,每次给定一长度为 \(n\) 的数列 \(a\),参数 \(m\)。
判断能否通过重新排列 \(a\),使得下式成立:\[\sum_{i=1}^{n}\sum_{j=i}^{n} \frac{a_j}{j} = m \]上式中均为实数运算,不存在取整。
\(1\le t, n\le 100\),\(0\le m,a_i\le 10^6\)。
1S,256MB。
无论 \(a\) 如何排列,都有:
判断 \(\sum_{i=1}^{n}a_i = m\) 是否成立即可。
//知识点:结论
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
//=============================================================
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) {
w = (w << 3) + (w << 1) + (ch ^ '0');
}
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
//=============================================================
int main() {
int t = read();
while (t --) {
int n = read(), m = read();
for (int i = 1; i <= n; ++ i) {
m -= read();
}
printf("%s\n", !m ? "YES" : "NO");
}
return 0;
}
B
\(t\) 组数据,每次给定参数 \(n\),要求构造一个 \(n\times n\) 的数字矩阵 \(a\),满足下列要求:
- \(0\le a_{i,j}\le 10^5\)。
- \(a_{i,j}\notin \mathbb{P}\)。
- \(\forall 1\le x\le n\),满足 \(\sum_{i=1}^{n} a_{x,i} \in \mathbb{P}\),\(\sum_{i=1}^{n} a_{i,x} \in \mathbb{P}\)。
其中 \(\mathbb {P}\) 表示质数集。
\(1\le t\le 10\),\(2\le n\le 100\)。
1.5S,256MB。
怀疑 \(a_{i,j}\le 10^5\) 是因为 SPJ 跑的有点慢。
以下是场上的构造方法。
一个显然想法是使各行各列的和都相等,显然可以构造成这样的形式:
数据范围不大,暴力找到一个非质数 \(x\),使 \(x +(n-1)\in \mathbb {P}\) 即可。
//知识点:构造
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int p_num, p[kN], las[kN];
bool vis[kN];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) {
w = (w << 3) + (w << 1) + (ch ^ '0');
}
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void Prepare() {
for (int i = 2; i <= 100000; ++ i) {
if (! vis[i]) p[++ p_num] = i;
for (int j = 2 * i; j <= 100000; j += i) {
vis[j] = true;
}
}
}
//=============================================================
int main() {
Prepare();
int t = read();
while (t --) {
int n = read(), ans;
for (int i = 1; i <= p_num; ++ i) {
ans = p[i];
if (ans <= (n - 1)) continue ;
if (vis[ans - (n - 1)]) break ;
}
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= n; ++ j) {
if (i == j) {
printf("%d ", ans - (n - 1));
} else {
printf("%d ", 1);
}
}
printf("\n");
}
}
return 0;
}
C
以下是一份在下标从 \(0\) 开始的数组 \(a\) 中二分查找 \(x\) 是否存在的代码:
给定参数 \(n,x,pos\),求有多少个 \(1\sim n\) 的排列,满足在排列中进行上述二分查找算法,最后能在 \(pos\) 位置找到 \(x\),答案对 \(10^9+7\) 取模。
\(1\le x\le n\le 10^3\),\(0\le pos\le n-1\)。
1S,256MB。
已知查找的终止状态,可以还原出二分各步访问到的位置,以及它们与 \(x\) 的相对大小关系。
设 \(a\) 表示访问到的位置中 小于 \(x\) 的位置的数量,\(b\) 表示 大于 \(x\) 的数量,答案显然为:
给出的程序中出现 \(a_{mid}=x\) 时仍然会继续二分,代码里的 down
实际上表示 小于等于 \(x\) 的位置的数量。
时间复杂度 \(O(n)\)。
//知识点:组合数学
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const LL mod = 1e9 + 7;
const LL kN = 1e3 + 10;
//=============================================================
int n, x, pos;
LL down, up, fac[kN], invfac[kN];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) {
w = (w << 3) + (w << 1) + (ch ^ '0');
}
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
LL QPow(LL x_, LL y_) {
LL ret = 1;
for (; y_; y_ >>= 1) {
if (y_ & 1) ret = ret * x_ % mod;
x_ = x_ * x_ % mod;
}
return ret;
}
LL A(LL n_, LL m_) {
if (n_ < 0 || m_ < 0 || n_ < m_) return 0ll;
return fac[n_] * invfac[n_ - m_] % mod;
}
//=============================================================
int main() {
invfac[0] = fac[0] = 1;
n = read(), x = read(), pos = read();
for (int i = 1; i <= n; ++ i) {
fac[i] = 1ll * fac[i - 1] * i % mod;
// invfac[i] = QPow(fac[i], mod - 2);
}
invfac[n] = QPow(fac[n], mod - 2);
for (int i = n - 1; i >= 1; -- i) {
invfac[i] = 1ll * invfac[i + 1] * (i + 1) % mod;
}
int l = 0, r = n;
while (l < r) {
int mid = (l + r) / 2;
if (pos >= mid) {
down ++;
l = mid + 1;
} else {
up ++;
r = mid;
}
}
LL ans = A(x - 1, down - 1) * A(n - x, up) % mod;
ans = ans * A(n - down - up, n - down - up) % mod;
printf("%lld\n", ans);
return 0;
}
D
给定一棵 \(n\) 个节点的有根树,根为 \(1\),第 \(i\) 个节点上有 \(a_i\) 个人。
每个人可以往任意子节点走,直到走到叶节点,求最后人最多的叶节点的最少人数。
\(2\le n\le 2\times 10^5\),\(0\le a_i\le 10^9\)。
1S,256MB
场上乱搞了个假 DP 吃了两发= =
设 \(\operatorname{sum}_{u}\) 表示 \(u\) 子树中所有节点的人数之和,\(\operatorname{leaf}_u\) 表示 \(u\) 子树中叶节点的个数。
首先考虑最理想状态,对于节点 \(u\),若它子树中的所有人都能均匀地散布在所有叶节点中,则显然该子树中 人最多的叶节点的人数为 \(\left\lceil \frac{\operatorname{sum}_u}{\operatorname{leaf}_u} \right\rceil\)。
但一般无法达到理想状态,设 \(f_{u}\) 表示以 \(u\) 为根的子树中人最多的叶节点的人数。对于节点 \(u\) 的某儿子 \(v\),显然,存在 \(f_{v} > \left\lceil \frac{\operatorname{sum}_u}{\operatorname{leaf}_u} \right\rceil\) 时无法均分。
反之,当 \(\forall v\in son(u),\ f_v\le \left\lceil \frac{\operatorname{sum}_u}{\operatorname{leaf}_u} \right\rceil\) 时,可以将 \(a_u\) 按一定方案分配到各叶节点,形成均匀散布的形式。
对于所有叶节点 \(u\),初始化 \(\operatorname{leaf}_u = 1\),则有显然的状态转移方程:
答案即为 \(f_1\)。
算法总时间复杂度 \(O(n)\)。
还有种被卡 ull
的暴力二分答案,感兴趣的可以看下 Luogu 题解。
//知识点:树形DP
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <vector>
#include <cmath>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
int n, e_num, a[kN], head[kN], v[kN], ne[kN];
LL leaf[kN], sum[kN], f[kN];
bool fa[kN];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) {
w = (w << 3) + (w << 1) + (ch ^ '0');
}
return f * w;
}
void Chkmax(LL &fir_, LL sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(LL &fir_, LL sec_) {
if (sec_ < fir_) fir_ = sec_;
}
void AddEdge(int u_, int v_) {
v[++ e_num] = v_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
void Dfs(int u_) {
if (! fa[u_]) leaf[u_] = 1;
sum[u_] = a[u_];
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
Dfs(v_);
Chkmax(f[u_], f[v_]);
sum[u_] += sum[v_];
leaf[u_] += leaf[v_];
}
Chkmax(f[u_], ceil(1.0 * sum[u_] / leaf[u_]));
}
//=============================================================
int main() {
n = read();
for (int v_ = 2; v_ <= n; ++ v_) {
int u_ = read();
AddEdge(u_, v_);
fa[u_] = true;
}
for (int i = 1; i <= n; ++ i) a[i] = read();
Dfs(1);
printf("%lld\n", f[1]);
return 0;
}
E
Link。
给定一长度为 \(n\) 的数列 \(a\),求其所有连续子序列的 \(\operatorname{mex}\) 的 \(\operatorname{mex}\)。
\(\operatorname{mex}(S)\) 定义为数集 \(S\) 中最小的没有出现的 正整数。
\(1\le a_i\le n\le 10^5\)。
1S,256MB。
设答案为 \(ans\),显然答案符合下列性质:
- 没有连续子序列的 \(\operatorname{mex}\) 为 \(ans\)。
- \(1\sim ans-1\) 都能够作为某连续子序列的 \(\operatorname{mex}\) 出现。
先考虑如何判断性质 1。
若某连续的子序列的 \(\operatorname{mex}\) 为 \(ans\),显然该连续子序列中不包括数 \(ans\),且连续子序列越长,越有可能满足条件。
考虑枚举所有不含 \(ans\) 的极长连续子序列进行判断,发现这些连续子序列即为整个数列 被所有 \(ans\) 分割成的子段。查询这些子段的 \(\operatorname{mex}\),判断是否等于 \(ans\) 即可。
再考虑性质 2,发现只需要从小到大枚举答案,重复对性质 1 的判断即可。
枚举到的第一个不满足性质 1 的即为答案。
考虑实现。
对于一个在数列中出现了 \(k\) 次的数,它可将数列分为 \(k+1\) 段,则总查询 \(\operatorname{mex}\) 次数为 \(O(n)\) 级别。
使用主席树,总时间复杂度为 \(O(n\log n)\) 级别。
使用莫队 + 值域分块,总时间复杂度 \(O(n\sqrt m + m\sqrt n)\)。
怎么用主席树实现啊/jk
第 \(i\) 棵主席树的叶节点 \(x\) 储存权值 \(x\) 在 \(a_{1}\sim a_{i}\) 中最晚出现的位置。 插入时只有单点修改,因此可以可持久化。
查询时查询第 \(r\) 棵主席树中最小的,最晚出现位置 \(<l\) 的权值,即为区间 \([l,r]\) 的 \(\operatorname{mex}\)。线段树维护区间最小值,主席树上二分即可。
//知识点:结论,主席树
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <vector>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n, a[kN], root[kN];
std::vector <int> pos[kN];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) {
w = (w << 3) + (w << 1) + (ch ^ '0');
}
return f * w;
}
void Chkmax(int &fir_, int sec_) {
if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
if (sec_ < fir_) fir_ = sec_;
}
namespace Hjt {
#define ls (lson[now_])
#define rs (rson[now_])
#define mid (L_+R_>>1)
int node_num, lson[kN << 5], rson[kN << 5], t[kN << 5];
void Pushup(int now_) {
t[now_] = std::min(t[ls], t[rs]);
}
void Insert(int &now_, int pre_, int L_, int R_, int pos_, int val_) {
now_ = ++ node_num;
if (L_ == R_) {
t[now_] = val_;
return ;
}
ls = lson[pre_], rs = rson[pre_];
t[now_] = t[pre_];
if (pos_ <= mid) Insert(ls, lson[pre_], L_, mid, pos_, val_);
else Insert(rs, rson[pre_], mid + 1, R_, pos_, val_);
Pushup(now_);
}
int Query(int now_, int L_, int R_, int pos_) {
if (L_ == R_) return L_;
if (t[ls] < pos_) return Query(ls, L_, mid, pos_);
return Query(rs, mid + 1, R_, pos_);
}
#undef ls
#undef rs
#undef mid
}
//=============================================================
int main() {
n = read();
for (int i = 1; i <= n; ++ i) {
a[i] = read();
pos[a[i]].push_back(i);
Hjt::Insert(root[i], root[i - 1], 1, n + 1, a[i], i);
}
for (int i = 1; i <= n + 1; ++ i) pos[i].push_back(n + 1);
for (int i = 1; i <= n + 2; ++ i) {
int lim = pos[i].size(), flag = 0;
for (int j = 0, las = 1; j < lim; ++ j) {
int now = pos[i][j];
if (las < now) {
flag = (Hjt::Query(root[now - 1], 1, n + 1, las) == i);
if (flag) break;
}
las = now + 1;
}
if (! flag) {
printf("%d\n", i);
break;
}
}
return 0;
}
F
感觉是个 nb 题,所以先咕掉了。
总结
- \(1\sim n\) 的阶乘的逆元可以 \(O(\log n + n)\) 地处理出来。
- 少一点分割线 markdown 会更好看。
- 只要是单点修改的信息都可以进行可持久化。