Educational Codeforces Round 3
题目链接:https://codeforces.com/contest/609
A - USB Flash Drives
送分题。
B - The Best Gift
送分题。
C - Load Balancing
题意:给一个数组,每次可以选一对数字,分别进行+1-1。使用最少次数的操作,达到最小的极差。
题解:最小的极差要么是0要么是1。可以算出sum,然后算出上平均值和下平均值的值(和个数?根本不关心个数),把<下平均值的都变成下平均值,大于上平均值的都变成上平均值。
int a[100005];
void test_case() {
int n;
scanf("%d", &n);
ll sum = 0;
for(int i = 1; i <= n; ++i) {
scanf("%d", &a[i]);
sum += a[i];
}
ll LM = sum / n;
ll RM = LM + 1;
int cntRM = sum % n;
ll ans = 0;
for(int i = 1; i <= n; ++i) {
if(cntRM > 0 && a[i] >= RM) {
ans += a[i] - RM;
--cntRM;
} else if(a[i] >= LM)
ans += a[i] - LM;
}
printf("%lld\n", ans);
}
启示:注意只需要统计被-1的那一半就可以了,贪心选择能变成上平均值的先变完上平均值,剩下的全部都要变成下平均值(含排在后面的多出来的上平均值)。假算法用assert就知道假在哪里,相当于可以看到一点点数据。
*D - Gadgets for dollars and pounds
题意:有n天,给出n天的卢布到美元或者英镑的汇率,共有m种商品,每种商品是固定的美元价格和固定的英镑价格。你有s卢布,要求买k种商品。问是否可行,不可行输出-1。否则输出最小的需要的天数d,然后输出所购买的k种商品的编号以及他们分别在哪天购买。多种方案输出任意一种。
题解:假如不要求最小的需要的天数d,莫非随便dp一下:设dp[i][j]为前i种商品中购买j种商品所需的最小价格,则转移为dp[i][j]=min(dp[i-1][j],dp[i][j-1]+mincost[i])?复杂度直接爆炸,但是很明显就不需要dp,因为明显可以贪心取出来。所以观察到这一点之后就懂了,首先最小的天数d满足单调性,因为天数更长只会让商品的最低价更有机会下降。所以可以二分枚举d,那么只需要验证d天内是否可以买k种商品。从前缀最小值中取出前d天中,单位美元所需的最少的卢布数,以及单位英镑所需的最少的英镑数。然后用这个数取遍历所有商品求出对应的最低的卢布价格,按卢布价格排序然后贪心取前面的前k个出来计算总价是否超过s。难度估计1800?
仔细看输入发现看错题了,每种商品要么只能用美元买,要么只能用英镑买,问题不大。然后在输出的时候复杂度爆炸了,发现之后改过来,问题不大。发现忘记nth_element怎么用了,用个[1,100]数组c进行random_shuffle之后用nth_element进行测试,要使得c[k]==k成立的写法就是nth_element(c+1,c+k,c+1+n)。换句话说,从1开始计数的数组,取出第k小(k也是从1开始计数)需要用nth_element(c+1,c+k,c+1+n)。调用结束之后,恰好会使得c[k]就是数组的第k小(k也是从1开始计数)。
仔细想想有很多地方可以省掉内存。
假如用排序的话会多个log,但是这个log没有取满,大概只增加了40%的性能消耗。远远不及2000%。我猜测应该有一种类似“根号平衡”的策略,每次枚举的并不是中点,而是中点偏右的某个点,这样会不会使得期望复杂度总和下降呢?需要验证这个思路,要知道枚举一个答案,这个答案的正确概率有多大。也就是说要知道答案取值的概率分布。在这道题里面,枚举的值越大,答案是YES的概率也会上升。感觉这个问题应该蛮复杂的,有没有神仙可以解决这个问题。所以说这种带log的复杂度都是很玄学的,只能说使用kth_element的最坏情况下真的会取到mlogn,但是实际上跑起来却快得飞起。
int n, m, k, s;
int a[200005];
int b[200005];
struct Node {
int id;
int t;
int c;
ll s;
Node() {}
Node(int id, int t, int c, ll s): id(id), t(t), c(c), s(s) {}
bool operator<(const Node& nd)const {
return s < nd.s;
}
} nd[200005];
bool check(int day) {
int mina = a[day];
int minb = b[day];
for(int i = 1; i <= m; ++i) {
if(nd[i].t == 1)
nd[i].s = 1ll * nd[i].c * mina;
else
nd[i].s = 1ll * nd[i].c * minb;
}
nth_element(nd + 1, nd + k, nd + 1 + m);
ll sum = 0;
for(int i = 1; i <= k; ++i) {
sum += nd[i].s;
if(sum > s)
return false;
}
return true;
}
int bs() {
int L = 1, R = n;
while(1) {
int M = (L + R) >> 1;
if(L == M) {
if(check(L))
return L;
if(R != L && check(R))
return R;
return -1;
}
if(check(M))
R = M;
else
L = M + 1;
}
}
void test_case() {
scanf("%d%d%d%d", &n, &m, &k, &s);
for(int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
for(int i = 2; i <= n; ++i)
a[i] = min(a[i], a[i - 1]);
for(int i = 1; i <= n; ++i)
scanf("%d", &b[i]);
for(int i = 2; i <= n; ++i)
b[i] = min(b[i], b[i - 1]);
for(int i = 1; i <= m; ++i) {
int t, c;
scanf("%d%d", &t, &c);
nd[i] = Node(i, t, c, 0);
}
int res = bs();
if(res == -1) {
printf("-1\n");
return;
}
printf("%d\n", res);
int daya = 0, dayb = 0;
for(int i = 1; i <= k; ++i) {
int day = 0;
if(nd[i].t == 1) {
if(daya == 0) {
int cost = nd[i].s / nd[i].c;
for(int i = 1; i <= n; ++i) {
if(a[i] == cost) {
daya = i;
break;
}
}
}
day = daya;
} else {
if(dayb == 0) {
int cost = nd[i].s / nd[i].c;
for(int i = 1; i <= n; ++i) {
if(b[i] == cost) {
dayb = i;
break;
}
}
}
day = dayb;
}
printf("%d %d\n", nd[i].id, day);
}
}
*E - Minimum spanning tree for each edge
标签:最小生成树,树剖,ST表。
题意:给一个n个点m条边的边带权无向图,对于每条边,求包含这条边在内的最小生成树的权值和。
题解:这道题好像做过吧?求出整个图的最小生成树,对于某条边,假如他就在树上,则返回当前的权值和。否则要包含这条边(记为e(u,v)),则要断开u<->v路径上的一个权值最大的边。上面的两种情况可以合并,因为假如,某条边就在树上,那么权值最大的边就是他本身。所以变成一个在树上查询路径的最大值的问题,可以用树剖线段树或者树剖ST表去做。复制以前写过的那道题过来。
https://www.cnblogs.com/KisekiPurin2019/p/12274986.html#_label4
不过这个200行还是有点夸张。
#define lc (o<<1)
#define rc (o<<1|1)
const int MAXN = 200000 + 5;
int dep[MAXN], siz[MAXN], son[MAXN], fa[MAXN], top[MAXN], tid[MAXN], rnk[MAXN], cnt;
int n, m, r, mod;
int a[MAXN];
int head[MAXN], etop;
struct Edge {
int v, next;
int w;
} e[MAXN * 2];
inline void init(int n) {
etop = 0;
memset(head, -1, sizeof(head[0]) * (n + 1));
}
inline void addedge(int u, int v, int w) {
e[++etop].v = v;
e[etop].w = w;
e[etop].next = head[u];
head[u] = etop;
e[++etop].v = u;
e[etop].w = w;
e[etop].next = head[v];
head[v] = etop;
}
int faE[MAXN + 5];
struct SparseTable {
static const int MAXLOGN = 19;
static const int MAXN = 200000;
int n, logn[MAXN + 5];
int f[MAXN + 5][MAXLOGN + 1];
void Init1() {
logn[1] = 0;
for(int i = 2; i <= MAXN; i++)
logn[i] = logn[i >> 1] + 1;
}
void Init2(int _n) {
n = _n;
for(int i = 1; i <= n; i++)
//树剖时应替换本身在线段树中build位置的东西
f[i][0] = faE[rnk[i]];
for(int j = 1, maxlogn = logn[n]; j <= maxlogn; j++) {
for(int i = 1; i + (1 << j) - 1 <= n; i++)
f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
}
}
int Query(int l, int r) {
int s = logn[r - l + 1];
return max(f[l][s], f[r - (1 << s) + 1][s]);
}
} st;
void init1() {
dep[r] = 1;
}
void dfs1(int u, int t, int w) {
siz[u] = 1, son[u] = -1, fa[u] = t;
faE[u] = w;
for (int i = head[u]; i != -1; i = e[i].next) {
int v = e[i].v;
if(v == t)
continue;
dep[v] = dep[u] + 1;
dfs1(v, u, e[i].w);
siz[u] += siz[v];
if (son[u] == -1 || siz[v] > siz[son[u]])
son[u] = v;
}
}
void init2() {
cnt = 0;
}
void dfs2(int u, int t) {
top[u] = t;
tid[u] = ++cnt;
rnk[cnt] = u;
if (son[u] == -1)
return;
dfs2(son[u], t);
for (int i = head[u]; i != -1; i = e[i].next) {
int v = e[i].v;
if(v == fa[u] || v == son[u])
continue;
dfs2(v, v);
}
}
int CPTQuery(int u, int v) {
int ret = -INF;
int tu = top[u], tv = top[v];
while (tu != tv) {
if(dep[tu] > dep[tv]) {
swap(u, v);
tu = top[u], tv = top[v];
}
//对边树剖,没到同一重链的时候,要算上轻重链交错位置
ret = max(ret, st.Query(tid[tv], tid[v]));
v = fa[tv];
tv = top[v];
}
//对边树剖,已到达同一重链上,不再算上高处的节点对应的边,也就是左区间+1,越界需要返回
if(tid[u] == tid[v])
return ret;
if(dep[u] > dep[v])
swap(u, v);
ret = max(ret, st.Query(tid[u] + 1, tid[v]));
return ret;
}
struct DisjointSetUnion {
static const int MAXN = 200000;
int n, fa[MAXN + 5], rnk[MAXN + 5];
void Init(int _n) {
n = _n;
for(int i = 1; i <= n; i++) {
fa[i] = i;
rnk[i] = 1;
}
}
int Find(int u) {
int r = fa[u];
while(fa[r] != r)
r = fa[r];
int t;
while(fa[u] != r) {
t = fa[u];
fa[u] = r;
u = t;
}
return r;
}
bool Merge(int u, int v) {
u = Find(u), v = Find(v);
if(u == v)
return false;
else {
if(rnk[u] < rnk[v])
swap(u, v);
fa[v] = u;
rnk[u] += rnk[v];
return true;
}
}
} dsu;
struct E2 {
int w, u, v, id;
bool operator<(const E2 &e)const {
return w < e.w;
}
} e2[200005];
ll ans[200005];
void test_case() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= m; ++i) {
scanf("%d%d%d", &e2[i].u, &e2[i].v, &e2[i].w);
e2[i].id = i;
}
//建立最小生成树
sort(e2 + 1, e2 + 1 + m);
dsu.Init(n);
init(n);
int cnt = 0;
ll sum = 0;
for(int i = 1; i <= m; ++i) {
if(dsu.Merge(e2[i].u, e2[i].v)) {
addedge(e2[i].u, e2[i].v, e2[i].w);
sum += e2[i].w;
++cnt;
if(cnt == n - 1)
break;
}
}
//对最小生成树进行树剖
init1();
int r = 1;
dfs1(r, -1, -INF);
init2();
dfs2(r, r);
st.Init1();
st.Init2(n);
//统计答案
for(int j = 1; j <= m; ++j) {
int ret = CPTQuery(e2[j].u, e2[j].v);
ll tmp = sum - ret + e2[j].w;
ans[e2[j].id] = tmp;
}
for(int j = 1; j <= m; ++j)
printf("%lld\n", ans[j]);
return;
}
好像有更简单的方法,我们引入树剖的想法是在于使用线段树来支持修改,既然是不需要修改的,除了使用ST表改进复杂度以外,还可以使用树上倍增来求。每次在倍增求LCA的时候顺便维护到LCA的边的路径上的最大值。
int n, m;
struct E {
int w, u, v, id;
bool operator<(const E &e)const {
return w < e.w;
}
} e[200005];
struct DisjointSetUnion {
static const int MAXN = 200000;
int n, fa[MAXN + 5], rnk[MAXN + 5];
void Init(int _n) {
n = _n;
for(int i = 1; i <= n; i++) {
fa[i] = i;
rnk[i] = 1;
}
}
int Find(int u) {
int r = fa[u];
while(fa[r] != r)
r = fa[r];
int t;
while(fa[u] != r) {
t = fa[u];
fa[u] = r;
u = t;
}
return r;
}
bool Merge(int u, int v) {
u = Find(u), v = Find(v);
if(u == v)
return false;
else {
if(rnk[u] < rnk[v])
swap(u, v);
fa[v] = u;
rnk[u] += rnk[v];
return true;
}
}
} dsu;
vector<pii> G[200005];
ll MST() {
sort(e + 1, e + 1 + m);
dsu.Init(n);
ll sum = 0;
for(int i = 1, cnt = 0; i <= m; ++i) {
int u = e[i].u, v = e[i].v, w = e[i].w;
if(dsu.Merge(u, v)) {
G[u].push_back({v, w});
G[v].push_back({u, w});
sum += w;
++cnt;
if(cnt == n - 1)
break;
}
}
return sum;
}
int dep[200005];
int fa[200005][20 + 1];
int maxw[200005][20 + 1];
void dfs(int u, int p, int faw) {
dep[u] = dep[p] + 1;
fa[u][0] = p;
maxw[u][0] = faw;
for(int i = 1; i <= 20; ++i) {
fa[u][i] = fa[fa[u][i - 1]][i - 1];
maxw[u][i] = max(maxw[u][i - 1], maxw[fa[u][i - 1]][i - 1]);
}
for(auto &e : G[u]) {
if(e.first == p)
continue;
dfs(e.first, u, e.second);
}
}
int LCA(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
int res = -INF;
for (int i = 20; i >= 0; i--) {
if (dep[x] - (1 << i) >= dep[y]) {
res = max(res, maxw[x][i]);
x = fa[x][i];
}
}
for (int i = 20; i >= 0; i--) {
if (fa[x][i] != fa[y][i]) {
res = max(res, maxw[x][i]);
res = max(res, maxw[y][i]);
x = fa[x][i], y = fa[y][i];
}
}
if(x == y)
return res;
//x和y不同,但拥有相同的0级父亲,补上两条边
return max(res, max(maxw[x][0], maxw[y][0]));
}
ll ans[200005];
void test_case() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= m; ++i) {
scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
e[i].id = i;
}
//建立最小生成树
ll sum = MST();
//对最小生成树进行树上倍增
dfs(1, 0, 0);
//统计答案
for(int j = 1; j <= m; ++j) {
int ret = LCA(e[j].u, e[j].v);
ll tmp = sum - ret + e[j].w;
ans[e[j].id] = tmp;
}
for(int j = 1; j <= m; ++j)
printf("%lld\n", ans[j]);
}
注意:在使用倍增求LCA时,执行完第一个循环必有x和y深度相等,此时若x和y相等(x和y的LCA是其中之一)则直接返回,否则执行最后一个循环,然后必定有x和y的父亲相等,但x与y不相等,则说明还剩下x和y连接他们的LCA的最后两条边没加进去。
*F - Frogs and mosquitoes
看起来很像一棵线段树。
题意:有很多只青蛙,第i只青蛙位于点xi,拥有长度为ti的右侧管辖范围,也就是[xi,xi+ti]。有若干只蚊子依次出现,第j只蚊子出现在点pj中。管辖范围包含pj的最左侧的青蛙会把这个蚊子吃掉,并且管辖范围的长度会增加这个蚊子的大小sj。若一只蚊子没有在出现时被吃掉,那么他会停留在原地。两只蚊子出现之间的间距很长,可以假设青蛙每次都会把可以吃掉的蚊子吃掉之后,下一只蚊子才会到来。
题解:虽然看起来很线段树,但是实现上有一些细节,懒得想了,按敦爷说的抄答案也是学习的一种。需要先观察到一点:某只蚊子假如没有及时被吃掉,那么之后只有管辖范围变长的青蛙才有机会继续吃掉新的蚊子。并且这个过程可以不断循环(也就是不断吃新的蚊子),也就是说,假如对蚊子排序,在某只青蛙的管辖范围变长之后,需要查询pj>=xi的第一只蚊子,比较一下这个青蛙能不能吃掉他,若不能就退出,否则就吃掉他并继续验证是否可以继续吃,所以这里要用一个支持lower_bound的结构,当然就是set。蚊子的大小可以另开数组存,或者干脆开个set
启示:区间的左端点不变,修改右端点的问题,很像线段树维护的东西。尤其像是“右端点>=x的左端点最小的区间”这种表述,用在线段树上二分就可以解决。需要注意没有被及时吃掉的蚊子,一定是在左边的某只青蛙的管辖范围变长的瞬间才有可能被吃掉,而且需要注意到可以人为给蚊子排序,每次取出最有机会被当前更新的青蛙吃掉的蚊子来验证。也就是说发现蚊子之间的次序关系,不要每次都暴力找所有的没被吃掉的蚊子。
const int MAXN = 200000;
struct Node {
int x;
int id;
ll t;
int cnt;
} nd[MAXN + 5];
bool cmpX(const Node& nd1, const Node& nd2) {
return nd1.x < nd2.x;
}
bool cmpID(const Node& nd1, const Node& nd2) {
return nd1.id < nd2.id;
}
struct SegmentTree {
#define ls (o<<1)
#define rs (o<<1|1)
ll ma[(MAXN << 2) + 5];
void PushUp(int o) {
ma[o] = max(ma[ls], ma[rs]);
}
void Build(int o, int l, int r) {
if(l == r)
ma[o] = nd[l].x + nd[l].t;
else {
int m = (l + r) >> 1;
Build(ls, l, m);
Build(rs, m + 1, r);
PushUp(o);
}
}
void Update(int o, int l, int r, int p, ll v) {
if(l == r) {
ma[o] = v;
return;
} else {
int m = (l + r) >> 1;
if(p <= m)
Update(ls, l, m, p, v);
if(p >= m + 1)
Update(rs, m + 1, r, p, v);
PushUp(o);
}
}
int Query(int o, int l, int r, int v) {
if(ma[o] < v)
return -1;
while(l != r) {
int m = (l + r) >> 1;
if(ma[ls] >= v) {
o = ls;
r = m;
} else {
o = rs;
l = m + 1;
}
}
return l;
}
#undef ls
#undef rs
} st;
map<int, pair<int, ll> > Map;
int n, m;
void Solve() {
int p, b;
scanf("%d%d", &p, &b);
int res = st.Query(1, 1, n, p);
if(res == -1 || nd[res].x > p) {
Map[p].first += 1;
Map[p].second += b;
return;
}
nd[res].cnt += 1;
nd[res].t += b;
while(1) {
auto it = Map.lower_bound(nd[res].x);
if(it == Map.end() || it->first > nd[res].x + nd[res].t)
break;
nd[res].cnt += it->second.first;
nd[res].t += it->second.second;
Map.erase(it);
}
st.Update(1, 1, n, res, nd[res].x + nd[res].t);
}
void test_case() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; ++i) {
scanf("%d%d", &nd[i].x, &nd[i].t);
nd[i].id = i, nd[i].cnt = 0;
}
sort(nd + 1, nd + 1 + n, cmpX);
st.Build(1, 1, n);
for(int i = 1; i <= m; ++i)
Solve();
sort(nd + 1, nd + 1 + n, cmpID);
for(int i = 1; i <= n; ++i)
printf("%d %lld\n", nd[i].cnt, nd[i].t);
}
标签:线段树上二分