暑假N天乐【比赛篇】 —— 2019杭电暑期多校训练营(第三场)
以下题解包括:
【1002】 支配树(拓扑+LCA) HDU-6604 Blow up the city
http://acm.hdu.edu.cn/showproblem.php?pid=6604
给定一个 \(n\) 个点 \(m\) 条边的有向无环图,出度为 0 的定义为“中心城市”,现有 \(q\) 次询问,每次给定两个起点 \(a\) 和 \(b\),每次可以选择一个点去掉,问存在几种方案,使得 \(a\) 和 \(b\) 任意一个无法到达“中心城市”。
支配树板子题?alright,并不会。学了一下发现好像大概就是拓扑排序+LCA+新图的深度检测,大概就是这样吧。
建立反向图,将城市到若干个终点看成从若干个起点出发到某个城市,再用一个源点连接那些度为 0 的点,即可看成从源点出发到某个城市。要炸掉一个点使得无法到达某个城市,那么需要炸掉的是从 源点到该城市的必经点,考虑建立支配树,根据定义可知支配树到根的链上结点个数就是必经点的个数。两个城市的容斥减去 LCA 到根上这条链即可。由于保证是 DAG ,因此直接按拓扑序建树即可,建完树利用结点的 \(dep\) 来求点到根的链长,注意最后答案要减去一开始添加的源点。
#include <map>
#include <set>
#include <list>
#include <cmath>
#include <ctime>
#include <deque>
#include <stack>
#include <queue>
#include <bitset>
#include <cctype>
#include <cstdio>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <numeric>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const double PI = acos(-1.0);
const double eps = 1e-6;
const int inf = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const int maxn = 1e5+5;
int n, m, q, cnt;
int dep[maxn], re_in[maxn]; // re_in 为反图点入度
int fa[maxn][20], tp[maxn];
vector<int> g[maxn], re_g[maxn], new_g[maxn]; // 正图、反图、新图
void init() {
for(int i = 0; i <= n; i++) {
g[i].clear();
re_g[i].clear();
new_g[i].clear();
dep[i] = 0;
re_in[i] = 0;
tp[i] = 0;
for(int j = 0; j <= log2(n); j++) {
fa[i][j] = 0;
}
}
cnt = 0;
}
void Topo() {
queue<int> q;
int rt = 0; // 图可能不连通,建一个新点连接起来
for(int i = 1; i <= n; i++) {
if(re_in[i] == 0) {
q.push(i);
new_g[rt].push_back(i);
g[i].push_back(rt);
}
}
while(!q.empty()) {
int u = q.front();
q.pop();
tp[++cnt] = u;
for(int i = 0; i < (int)re_g[u].size(); i++) {
int v = re_g[u][i];
re_in[v] --;
if(re_in[v] == 0) {
q.push(v);
}
}
}
}
int LCA(int x, int y) {
if(dep[x] < dep[y]) {
swap(x, y);
}
for(int i = 19; i >= 0; i--) {
if(dep[x] - (1<<i) >= dep[y]) {
x = fa[x][i];
}
}
if(x == y) {
return x;
}
for(int i = 19; i >= 0; i--) {
if(fa[x][i] != fa[y][i]) {
x = fa[x][i];
y = fa[y][i];
}
}
return fa[x][0];
}
void build() { // 建立支配树
for(int i = 1; i <= cnt; i++) {
int x = tp[i];
int y = g[x][0];
for(int j = 1; j < (int)g[x].size(); j++) {
int z = g[x][j];
y = LCA(y, z);
}
new_g[y].push_back(x);
dep[x] = dep[y] + 1;
fa[x][0] = y;
for(int i = 1; i <= 19; i++) {
fa[x][i] = fa[fa[x][i-1]][i-1];
}
}
}
int main() {
int t;
scanf("%d", &t);
while(t--) {
scanf("%d%d", &n, &m);
init();
for(int i = 1; i <= m; i++) {
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
re_g[v].push_back(u);
re_in[u] ++;
}
Topo();
build();
scanf("%d", &q);
while(q--) {
int a, b;
scanf("%d%d", &a, &b);
printf("%d\n", dep[a] + dep[b] - dep[LCA(a, b)]);
}
}
return 0;
}
【1004】 二分+权值线段树 HDU-6606 Distribution of books
http://acm.hdu.edu.cn/showproblem.php?pid=6606
给定一个 \(n\) 个数,可以选择其中 \(m\) 个数(不限)分成 k 堆,问每堆数字和最大值最小是多少。
以离散化后前缀和的 \(rk\) 为下标,\(f\) (最多能分成的块数)为权值建立线段树。线段树需要实现单点修改、区间求最大值操作。
时间复杂度:\(o(n*log^2(n))\) (不过我的代码好像是... \(o(n*log^3(n))\) 的,不过也过了就是)。
#include <map>
#include <set>
#include <list>
#include <cmath>
#include <ctime>
#include <deque>
#include <stack>
#include <queue>
#include <bitset>
#include <cctype>
#include <cstdio>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <numeric>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const double PI = acos(-1.0);
const double eps = 1e-6;
const ll inf = 200000000000000 + 5;
const int mod = 1e9 + 7;
const int maxn = 2e5+5;
int n, k, new_n;
int MAX[maxn<<2];
ll a[maxn], sum[maxn];
vector<ll> v;
int getid(ll x) {
return lower_bound(v.begin(), v.end(), x) - v.begin() + 1;
}
void build(int l, int r, int rt) {
MAX[rt] = -maxn;
if(l == r) {
return;
}
int mid = (l+r) >> 1;
build(l, mid, rt<<1);
build(mid+1, r, rt<<1|1);
}
void update(int l, int r, int rt, int pos, int v) {
if(l == r) {
MAX[rt] = v;
return ;
}
int mid = (l+r) >> 1;
if(pos <= mid) {
update(l, mid, rt<<1, pos, v);
}
else {
update(mid+1, r, rt<<1|1, pos, v);
}
MAX[rt] = max(MAX[rt<<1], MAX[rt<<1|1]);
}
int query(int L, int R, int l, int r, int rt) {
if(l > r) {
return -maxn;
}
if(L <= l && r <= R) {
return MAX[rt];
}
int mid = (l+r) >> 1;
int ans = -maxn;
if(L <= mid) {
ans = query(L, R, l, mid, rt<<1);
}
if(R > mid) {
ans = max(ans, query(L, R, mid+1, r, rt<<1|1));
}
return ans;
}
int check(ll x) {
build(1, new_n, 1);
int f;
for(int i = 1; i <= n; i++) {
int l = getid(sum[i]-x);
int r = new_n;
f = query(l, r, 1, new_n, 1) + 1;
if(sum[i]-x <= 0) {
f = max(f, 1);
}
if(f >= k) {
return 1;
}
if(f > 0) {
update(1, new_n, 1, getid(sum[i]), f);
}
}
return 0;
}
int main() {
int t;
scanf("%d", &t);
while(t--) {
v.clear();
sum[0] = 0;
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; i++) {
scanf("%lld", &a[i]);
sum[i] = sum[i-1] + a[i];
v.push_back(sum[i]);
}
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());
new_n = (int)v.size();
v.push_back(inf);
ll l = -inf;
ll r = inf;
while(l < r) {
ll mid = (l+r) >> 1;
int flag = check(mid);
if(flag) {
r = mid;
}
else {
l = mid+1;
}
}
printf("%lld\n", l);
}
return 0;
}
【1006】 数学 HDU-6608 Fansblog
http://acm.hdu.edu.cn/showproblem.php?pid=6608
队友敲的,雨我无瓜
给定一个数 \(p\) ,先找到比它小的第一个素数 \(q\),输出 \(q ! \ mod \ p\) 的答案。【\(1e^9 \leq p \leq 1e^{14}\)】
威尔逊定理 + 质数的密度分布。
威尔逊定理:判定一个自然数是否为素数的充分必要条件。即:当且仅当p为素数时:\((p - 1 )! ≡ -1 ( mod \ p )\)。
质数的密度分布:反正质数的差距大概不会超过2000。
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<queue>
#include<cmath>
#define mes(a, b) memset(a, b, sizeof a)
using namespace std;
typedef long long ll;
const int maxn = 1e7+10;
const int maxm = 1e6+10;
const int mod = 1e9+7;
int num[maxn], p[maxm], cnt;
ll n;
void init(){
cnt = 0;
for(ll i = 2; i < maxn; i++){
if(!num[i]){
p[++cnt] = i;
for(ll j = i*i*1ll; j < maxn; j+=i){
num[j] = 1;
}
}
}
}
ll mul(ll a, ll b) {
ll ans = 0;
while(b) {
if(b&1) {
ans = (ans + a) % n;
}
a = (a+a) % n;
b /= 2;
}
return ans;
}
ll pow(ll a, ll b){
ll ans = 1;
while(b){
if(b&1)
ans = mul(ans,a);
a = mul(a, a);
b /= 2;
}
return ans;
}
int main(){
init();
ll m, t;
scanf("%lld", &t);
while(t--){
scanf("%lld", &n);
int flag = 1;
for(m = n-1;flag; m--){
flag = 0;
for(int i = 1; i <= cnt && sqrt(m) >= p[i]; i++){
if(m % p[i]*1ll == 0){
flag = 1;
break;
}
}
if(flag == 0)
break;
}
m++;
ll num = 1;
for(; m < n; m++){
num = mul(num, m);
}
num = mul((n-1ll), pow(num, n-2));
printf("%lld\n", num%n);
}
return 0;
}
【1007】 权值线段树 HDU-6609 Find the answer
http://acm.hdu.edu.cn/showproblem.php?pid=6609
给定 \(n\) 个数和 \(m\) 的总容量(\(a_i <= m\))。对于每一个 \(i (1 \leq i \leq n)\),最少需要把区间 \([1,i-1]\) 内多少个数变成0,才能使得 \(\sum^{i}_{j=1} \leq m\)。 输出 n 个数表示答案。
比赛时候现实优先队列+multiset优化,然后 T 了,毕竟最差情况复杂度到 \(n^2\) 了。然后想主席树,怎么都写不出来。然后被旁边大佬启发才发现是权值线段树,然后就在主席树板子上改了改,就过了。
#include <map>
#include <set>
#include <list>
#include <cmath>
#include <ctime>
#include <deque>
#include <stack>
#include <queue>
#include <bitset>
#include <cctype>
#include <cstdio>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <numeric>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const double PI = acos(-1.0);
const double eps = 1e-6;
const int inf = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const int maxn = 2e5 + 5;
int n, m;
int a[maxn];
vector<int> v; // 保存去重后的数组
struct node {
int rk;
ll sum;
} T[maxn * 4];
int getid(int x) { // 获取每次更新的节点的“位置”
return lower_bound(v.begin(), v.end(), x) - v.begin() + 1;
}
void push_up(int rt) {
T[rt].rk = T[2*rt].rk + T[2*rt+1].rk;
T[rt].sum = T[2*rt].sum + T[2*rt+1].sum;
}
void update(int l, int r, int rt, int x) {
if(l == r) {
T[rt].rk ++;
T[rt].sum += v[l-1];
return ;
}
int mid = (l+r) / 2;
if(x <= mid) {
update(l, mid, 2*rt, x);
}
else {
update(mid+1, r, 2*rt+1, x);
}
push_up(rt);
}
int query(int l, int r, int rt, int x) {
if(l == r) {
return x / v[l-1];
}
int mid = (l+r) / 2;
if(T[2*rt].sum >= x) {
return query(l, mid, 2*rt, x);
}
else {
return T[2*rt].rk + query(mid+1, r, 2*rt+1, x-T[2*rt].sum);
}
}
int main() {
int t;
scanf("%d", &t);
while(t--) {
v.clear();
memset(T, 0, sizeof(T));
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
v.push_back(a[i]);
}
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end()); // 去重
int new_n = (int)v.size();
ll s = 0;
for(int i = 1; i <= n; i++) {
s += 1ll*a[i];
if(s <= 1ll*m) {
printf("0 ");
update(1, new_n, 1, getid(a[i]));
continue;
}
int x = m - a[i];
int RANK = query(1, new_n, 1, x);
printf("%d ", i-1-RANK);
update(1, new_n, 1, getid(a[i]));
}
printf("\n");
}
return 0;
}
【1009】 权值线段树 HDU-6611 Find the answer
http://acm.hdu.edu.cn/showproblem.php?pid=6611
参考:https://blog.csdn.net/baiyifeifei/article/details/97706037
给出一段序列,可以从中找出最多 \(k\) 段不下降子序列,使得权值和最大。
建立源点汇点和序列点,将所有的点拆成两个,分为母点和子点,母源点对子源点连接容量为k,费用为0的边,序列母点向序列子点连接容量为1,费用为序列上该点的值的负值,所有的序列子点向其后的权值大于等于它的序列点的母点连接容量为1,费用为0的边,所有的序列子点向母汇点连容量为1,费用为0的边,母汇点向子汇点连容量为k,费用为0的边,跑出最小费用后取负值即是答案。
又被人人都会的网络流卡了。\(dijkstra\) 优化的最小费用最大流,原理还不是太懂,就从网上扣了一个板子下来,算是先挖个坑把。
#include <map>
#include <set>
#include <list>
#include <cmath>
#include <ctime>
#include <deque>
#include <stack>
#include <queue>
#include <bitset>
#include <cctype>
#include <cstdio>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <numeric>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const double PI = acos(-1.0);
const double eps = 1e-6;
const int inf = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const int maxn = (2e3+5)*2;
struct EDGE {
int to, w, c, nxt;
};
vector<EDGE> g[maxn];
int n, k, tot; // tot 为拆点后的总点数
int a[maxn];
int H[maxn];
int dis[maxn];
int preV[maxn], preE[maxn]; // 前一个点,前一个边
void addedge(int u, int v, int w, int c) {
g[u].push_back({v, w, c, (int)g[v].size()});
g[v].push_back({u, 0, -c, (int)g[u].size()-1});
}
void MCMF(int s, int t, int f) { // 最小费用最大流
int ans = 0;
memset(H, 0, sizeof(H));
while(f) {
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>> > q;
memset(dis, inf, sizeof(dis));
dis[s] = 0;
q.push(pair<int, int>(0, s));
while(!q.empty()) {
pair<int, int> x = q.top();
q.pop();
int u = x.second;
if(dis[u] < x.first) {
continue;
}
for(int i = 0; i < (int)g[u].size(); i++) {
EDGE& e = g[u][i];
if(e.w > 0 && dis[e.to] > dis[u]+e.c+H[u]-H[e.to]) {
dis[e.to] = dis[u]+e.c+H[u]-H[e.to];
preV[e.to] = u;
preE[e.to] = i;
q.push(pair<int, int>(dis[e.to], e.to));
}
}
}
if(dis[t] == inf) {
break;
}
for(int i = 0; i <= tot; i++) {
H[i] += dis[i];
}
int d = f;
for(int v = t; v != s; v = preV[v]) {
d = min(d, g[preV[v]][preE[v]].w);
}
f -= d;
ans += d*H[t];
for(int v = t; v != s; v = preV[v]) {
EDGE& e = g[preV[v]][preE[v]];
e.w -= d;
g[v][e.nxt].w += d;
}
}
printf("%d\n", -ans);
}
int main() {
int t;
scanf("%d", &t);
while(t--) {
scanf("%d%d", &n, &k);
for(int i = 2; i <= n+1; i++) {
scanf("%d", &a[i]); // 从 2 开始存
}
int ss = 0, s = 1; // 源点拆
int t = 2+2*n, tt = 2+2*n+1; // 汇点拆
tot = tt+1;
for(int i = 0; i <= tot; i++) {
g[i].clear();
}
addedge(ss, s, k, 0); // 母源-->子源 连接容量为 k,费用为 0 的边
addedge(t, tt, k, 0); // 子汇-->母汇 连接容量为 k,费用为 0 的边
for(int i = 2; i <= n+1; i++) {
addedge(s, i, 1, 0); // 子源 --> 序列母点 连接容量为 1,费用为 0 的边
addedge(i, i+n, 1, -a[i]); // 序列母点 --> 序列子点 连接容量为 1,费用为 -ai 的边
addedge(i+n, t, 1, 0); // 序列子点 --> 子汇 连接容量为 1,费用为 0 的边
for(int j = i+1; j <= n+1; j++) {
if(a[j] >= a[i]) {
addedge(i+n, j, 1, 0); // 满足条件 --> 前一点的子点到后一点的母点 连接容量为 1,费用为 0 的边
}
}
}
MCMF(ss, tt, inf);
}
return 0;
}