NOIP模拟赛R8
A
绷,看错题导致自己被硬控 1 个小时。
其实也还好,题目问你最多可以被分成多少段,按照贪心不难想到要尽可能让每一段的和变小。
这个时候考虑前缀和 \(sum_i\),不难发现,如果要一段的和 \(\geq 0\),只需要让 $sum_i \geq sum_j $ 并且 \(i > j\)。
于是考虑邪修方法从后往前看,发现只需要记录当前看到的所有数字的最小值的位置,然后统计个数即可。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
const int INF = 1e9+100;
void solve () {
int n; cin >> n;
vector <int> a(n+1), sum(n+1);
for (int i = 1;i <= n;i++) {
cin >> a[i];
sum[i] = sum[i-1] + a[i];
}
vector <int> ans;
int minn = INF;
for (int i = n;i >= 1;i--)
if (sum[i] <= minn) ans.push_back(sum[i]), minn = sum[i];
cout << ans.size() << "\n";
}
int main () {
ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int _ = 1;
while (_--) solve();
return 0;
}
B
又是被图论杀死的一天。
手推样例,不难发现最优解就是从一个点出发到所有点然后再从所有点返回,而且不是 bfs
那种一层层往下递进的,而是一个一个点地走,类似拓扑排序,事实上还真是拓扑。
假设一个子树的根节点为 \(u\),不难发现,如果从 \(u\) 开始拓扑和从叶子节点开始拓扑,都会有一样数量的答案,而这两种情况的答案可以随机组合,所以不难发现对于一个子树而言,他的答案就是拓扑序的数量的平方。
拓扑序的公式我们先记住吧,\(\frac{size_u!}{\prod\text{u的所有子树的大小}}\)。
所以我们可以用 \(dp_i\) 表示以 \(i\) 为根的子树的 \(\prod\text{u的所有子树大小}\) 的逆元,然后对于每个节点去做一次换根 dp
,转移方程挺好想,因为当转移到根节点的一个儿子时,以那个儿子为根的子树已经变为了整棵树,而这个逆元我们可以不动,先把再转移前搞得儿子的子树节点数量的逆元给他抵消,然后加上新的以原来根节点为根的子树的逆元即可(这里 \(T_u\) 表示 \(u\) 的子树集合):
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
const ll MOD = 1e9+7;
const ll N = 2e5+1000;
ll fac[N], inc[N];
void solve () {
ll n; cin >> n;
vector <vector <ll>> E(n+1);
vector <ll> dp(n+1), si(n+1);
for (ll i = 1;i < n;i++) {
ll a, b; cin >> a >> b;
E[a].push_back(b), E[b].push_back(a);
}
ll root = (n+1)/2;
ll ans = 0;
auto dfs = [&](auto self, ll fa, ll now) -> void {
dp[now] = si[now] = 1;
for (auto to : E[now]) {
if (to == fa) continue;
self(self, now, to);
dp[now] = (dp[now]*dp[to])%MOD;
si[now] += si[to];
}
dp[now] = (dp[now]*inc[si[now]]%MOD)%MOD;
};
auto count = [&](auto self, ll fa, ll now, ll tmp) -> void {
ans = (ans + tmp*tmp%MOD) % MOD;
ll _tmp = 0;
for (auto to : E[now]) {
if (to == fa) continue;
_tmp = (tmp*inc[n-si[to]]%MOD*si[to])%MOD;
self(self, now, to, _tmp);
}
};
dfs(dfs, -1, root);
count(count, -1, root, dp[root]*fac[n]%MOD);
cout << ans%MOD << "\n";
}
int main () {
ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
fac[0] = fac[1] = inc[1] = 1;
for (ll i = 2;i < N;i++)
fac[i] = (i*fac[i-1])%MOD,
inc[i] = (MOD-MOD/i)*inc[MOD%i]%MOD;
ll _ = 1; while (_--) solve();
return 0;
}
C
逆天东西。
类似扫描线的思路?反正得先离散坐标,然后从左到右逐个删掉左侧的点,统计出包含任意点的矩形数量,需要用乘法原理。这里插播一条,假设有能分别包含两个点的 \(y\) 范围为 \([top_1, bottom_1], [top_2, bottom_2]\),不难发现如果假设底下的方案数为 \(cnt_i\),有结果:
所以可以用乘法分配律,直接全部加上即可。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define pii pair <int, int>
const int MOD = 1e9+7;
void solve () {
int n, m, k; cin >> n >> m >> k;
vector <pii> pos(k+10);
vector <int> val(k+10);
for (int i = 1;i <= k;i++) {
cin >> pos[i].first >> pos[i].second;
val[i] = pos[i].first;
}
sort(pos.begin()+1, pos.begin()+k+1,
[&](pii x, pii y) { return (x.first != y.first ? x.first < y.first : x.second < y.second); } );
sort(val.begin()+1, val.begin()+k+1);
int tot = unique(val.begin()+1, val.begin()+1+k)-val.begin();
// cout << tot << "\n";
val[0] = 0, val[tot] = n+1;
ll ans = 0;
for (int i = 1;i < tot;i++) {
ll tmp = 0;
int pre = val[i] - val[i-1];
int r = 1;
while (r <= k && pos[r].first < val[i]) r++;
set <int> s;
s.insert(0), s.insert(m+1);
for (int j = i;j < tot;j++) {
int top, bot;
for (;r <= k && pos[r].first == val[j];++r) {
if (s.count(pos[r].second)) continue;
set <int>::iterator now = s.lower_bound(pos[r].second);
top = *now, bot = *(--now);
tmp = (tmp + (ll)(top-pos[r].second)*(pos[r].second-bot)
%MOD)%MOD;
s.insert(pos[r].second);
}
int _tmp = val[j+1]-val[j];
ans = (ans + tmp*_tmp%MOD*pre%MOD)%MOD;
}
}
cout << ans << "\n";
}
int main () {
ios::sync_with_stdio(false);
cin.tie(nullptr), cout.tie(nullptr);
int _ = 1; while (_--) solve();
return 0;
}