Edu38
基本情况
C又不是正解,A甚至还加一,以后要考虑好再交。
C. Constructing Tests
为了更好的代码实现而非手算出解来推式子
式子都推出来了。
$n^2 - {\left \lfloor \frac{n}{m} \right \rfloor}^2 = x $
myCode
不知道怎么直接解,就写了一个 \(\operatorname{O}(n\log n)\) 的枚举 \(m\) 然后对 \(n\) 二分的写法。
constexpr int N(1E9 + 1);
void solve() {
#define tests
int x;
std::cin >> x;
if (x == 0) {
std::cout << "1 1\n";
return ;
}
for (int m = 1; m * m <= x * 10; m++) {
i64 lo(m), hi(N);
auto check = [&](auto& o) -> bool {
return o * o - (o / m) * (o / m) < x;
};
i64 n(0);
while (lo <= hi) {
i64 mid(lo + hi >> 1);
if (check(mid)) {
lo = mid + 1;
}
else {
hi = mid - 1;
n = mid;
}
}
if (n * n - (n / m) * (n / m) == x) {
std::cout << n << ' ' << m << '\n';
return ;
}
}
error;
}
STD
我疑惑的点在于如果按照求根公式那一套来解,代码很难实现,而且还要考虑下取整。
实际上可以暴力一点,先因式分解一下(为了方便代码实现服务)
\(x_1 = (n + {\left \lfloor \frac{n}{m} \right \rfloor})\)
\(x_2 = (n - {\left \lfloor \frac{n}{m} \right \rfloor})\)
然后直接 \(\operatorname{O}(\sqrt x)\) 枚举 \(x1, x2\)。
然后通过 \(x_1, x_2\),解出 \(n, m\)(暂时不用管向下取整等等)
最后再回代验证,就确保了正确性。
void solve() {
#define tests
int x;
std::cin >> x;
if (x == 0) {
std::cout << "1 1\n";
return ;
}
for (int i = 1; i * i < x; i++) if (x % i == 0) {
int x_1(x / i), x_2(i);
int n(x_1 + x_2 / 2), m((x_1 + x_2) / (x_1 - x_2));
if (n * n - (n / m) * (n / m) == x) {//回代验证
std::cout << n << ' ' << m << '\n';
return ;
}
}
error ;
return ;
}
D. Buy a Ticket
建图技巧:虚点
化点权为边权
暴力的想法是对每一个点跑单源最短路,肯定超时。
考虑优化,因为每条路径最后一定会加上 \(a_j\),也就是终点的点权,我们直接建立一个虚点,对所有城市连边,其边权就是每个城市的点权。
然后直接从虚点出发跑一次单源最短路,自然就得到了所有点的最短路径(从终点到起点的)。
struct Node {
i64 v;
i64 w;
};
struct State{
i64 u, d;
bool operator < (const State &s) const {
return d > s.d;
}
};
int n, m;
std::vector<std::vector<Node>> adj;
std::vector<i64> dis;
void dijkstra(int S){
std::priority_queue<State> heap;
dis.assign(n + 1, LONG_LONG_MAX);
heap.push({S, 0});
dis[S] = 0;
while(not heap.empty()){
auto[u, d](heap.top());
heap.pop();
if (d > dis[u]) continue;
for (auto&[v, w] : adj[u]) {
if (dis[u] + w < dis[v]) {
dis[v] = dis[u] + w;
heap.push({v, dis[v]});
}
}
}
}
void solve() {
std::cin >> n >> m;
adj.assign(n + 1, std::vector<Node>());
for (i64 i = 0, u, v, w; i < m; i++) {
std::cin >> u >> v >> w;
adj[u].push_back({v, w * 2});
adj[v].push_back({u, w * 2});
}
for (int i = 1; i <= n; i++) {
i64 w;
std::cin >> w;
adj[0].push_back({i, w});
}
dijkstra(0);
for (int i = 1; i <= n; i++) {
std::cout << dis[i] << ' ';
}
std::cout << '\n';
}
E. Max History
对于一个 \(n\) 的排列 \(a_1, a_2, \ldots, a_n\)。对于 \(i\) (从小到大枚举),如果 \(a_i > a_j\),则:\(\sum_{k=1}^{i-1} a_k\)。对 \(n\) 种可能的排列的 \(\sum_{k=1}^{n} a_k \bmod 10^9+7\)。
推式子
首先,由于总共 \(n!\) 种排列的 \(f(a)\) 难以分每次逐个统计,于是想到对每一个以 \(a_i\) 为终值的排列对答案的贡献分开计算并且求和。
而注意到,当 \(a_m >a_i\) 时,\(m=i\),由此可以得知:
如果将历史上出现的每一个 \(a_m\) 的值组成一个序列,则这个序列是单调递增的。
故 \(a_m\) 由 \(0\) 变为终值的过程中,只与那些比 \(a_m\) 小的数有关。
考虑先进行排序(时限3s),通过一次扫描对每个 \(a_i\) 得到比其值小的数字个数 \(pre_i\)。
下面就到式子了:
假设对于 \(a_i\) ,共有 \(x\) 个数比其小。
因为只有在 \(a_i\) 前的比起小的数会对结果有影响,故考虑枚举 \(a_i\) 前比其小的数字个数。
假设当前 \(a_i\) 前面有 \(i\) 个比它小的数,共 \(\binom{x}{i}\) 种方案。
这 \(i\) 个数随便排列,共 \(i!\) 种方案。
剩下除这 \(i\) 个数与当前枚举的 \(a_i\) 以外的数共 \(n-i-1\) 个随意排列方案数:\((n-i-1)!\) 。
所以数 \(a_i\) 对答案的贡献次数为:
下面的过程是简化这个式子:
拆组合数:
抵消:
将 \(x!\) 提到前面:
由组合数:\(\binom{n}{m}= \frac{n!}{m!(n-m)!}\) 添项:
将 \((n-x-1)!\) 提到前面:
合成组合数:
由于 \(i\) 与 \(x-i\) 值域均为 \(0 \sim x\),且\((i)+(x-i)=x\)。
所以 \(i\) 与 \(x-i\) 可相互替换(相当于求和的循环倒着枚举):
又因为:\(\sum_{i=0}^{k}\binom{n+i-1}{i}=\binom{n+k}{k}\)这一步利用了上项求和,关键在于多加了一项\(\binom{n}{-1} = 0\)
拆组合数:
抵消:
而因为最终值 \(a_m\) 不会被计算在答案当中,显然排序后所有值为 \(a_n\) 的数不应该被统计在答案当中。
则答案就为:
void solve() {
int n;
std::cin >> n;
std::vector<int> pre(n), a(n);
for (auto& x : a) std::cin >> x;
std::sort(all(a));
for (int i = 0; i + 1 < n; i++) {
if (a[i + 1] == a[i]) {
pre[i + 1] = pre[i];
}
else {
pre[i + 1] = i + 1;
}
}
Z ans(0);
Z fac(comb.fac(n));
for (int i = 0; i < n; i++) {
if (a[i] == a[n - 1]) {
break;
}
ans += fac / (Z(n) - Z(pre[i])) * Z(a[i]);
}
std::cout << ans << '\n';
}