初级组:

T1:

机器人每次跳正整数距离,若一共跳了 $k$ 次,距离分别为 $x_1,x_2,\ldots x_k$,则 $x_1 + x_2+\cdots+x_k = n$。
 

消耗的总电量为:$\sum_{i = 1}^{k}|a-x_i|$。

对于固定的 $k$,最小消耗就是 $|n - ka|$。
 

因为理想情况下每次都跳 $a$,总距离为 $ka$。为了把总距离调整成 $n$,至少需要修改 $|n - ka|$ 的距离,而这个下界一定能够达到。

所以问题变成:选择一个正整数 $k$,使 $|n - ka|$ 最小,也就是寻找距离 $n$ 最近的 $a$ 的正整数倍。

分类讨论。
当 $n < a$ 时 ,不能选择 $k = 0$,只能至少跳一次。直接跳 $n$:$\text{ans}=a - n$。

当 $n\ge a$ 时,令:$r=n\bmod a$。
 

有两种方法:

跳 ($\left\lfloor\dfrac na\right\rfloor$) 次,把其中一次增加 $r$,消耗 $r$。

跳 ($\left\lceil\dfrac na \right\rceil$) 次,把其中一次减少 $a - r$,消耗 $a - r$。

因此 $\text{ans}=\min(r, a-r)$。
 

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
	int T;
	cin >> T;
	while (T--) {
		ll n, a;
		cin >> n >> a;
		if (n < a) cout << a - n;
		else {
			ll r = n % a;
			cout << min(r, a - r);
		}
		cout << '\n';
	}
}
时间复杂度:$O(T)$。

T2:设相邻两次到达的节点距离为:$$d_i=\operatorname{dist}(p_i,p_{i+1})。$$

题目的条件就是:$d_1<d_2<\cdots<d_{k-1}。$

也就是说,每次移动的距离必须严格递增。

设树的直径长度为 $D$。

树上任意两点之间的距离都不超过 $D$,而每次移动距离都是正整数,因此严格递增的距离序列最多是:

$$
1,2,\ldots,D。
$$

所以最多有 $D$ 次移动,即:

$$
k\le D+1。
$$

取树上的一条直径,依次记直径上的节点为:

$$
v_0,v_1,\ldots,v_D。
$$

因为它们在同一条路径上,所以:

$$
\operatorname{dist}(v_x,v_y)=|x-y|。
$$

接下来只要排列下标 $0,1,\ldots,D$,使相邻下标差依次为 $1,2,\ldots,D$。

先考虑排列:

$$
0,D,1,D-1,2,D-2,\ldots
$$

它的相邻差依次为:

$$
D,D-1,\ldots,1。
$$

将这个排列倒过来,相邻差就变成:

$$
1,2,\ldots,D。
$$

因此一定能选出直径上的全部 $D+1$ 个节点,达到上界。

# 如何求直径

在树上进行两次 BFS:

1. 从节点 $1$ 出发,找到最远点 $s$。
2. 从 $s$ 出发,找到最远点 $t$,同时记录每个节点的父亲。
3. 从 $t$ 沿父亲一直回到 $s$,得到一条直径。

# 代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2e6 + 5;
int h[N], to[N << 1], nx[N << 1], cnt;
int q[N], d[N], fa[N], p[N];
void add(int u, int v) {
    to[++cnt] = v, nx[cnt] = h[u], h[u] = cnt;
}
int bfs(int s, int n) {
    memset(d, -1, (n + 1)* sizeof (int));
    int l = 0, r = 0, t = s;
    q[r++] = s;
    d[s] = 0;
    fa[s] = 0;
    while (l < r) {
        int u = q[l++];
        if (d[u] > d[t]) t = u;
        for (int i = h[u]; i; i = nx[i]) {
            int v = to[i];
            if (d[v] != -1) continue;
            d[v] = d[u] + 1;
            fa[v] = u;
            q[r++] = v;
        }
    }
    return t;
}
int main() {
    int n;
    cin >> n;
    for (int i = 1, u, v; i < n; i++) {
        cin >> u >> v;
        add(u, v), add(v, u);
    }
    int s = bfs(1, n), t = bfs(s, n);
    int m = 0;
    for (int x = t;; x = fa[x]) {
        p[m++] = x;
        if (x == s) break;
    }
    cout << m << endl;
    int mm = m - 1;
    for (int i = mm; i >= 0; i--) {
        int x = (i % 2) ? mm - (i >> 1) : (i >> 1);
        cout << p[x] << " ";
    }
}

时间复杂度:$O(n)$

T3/T1(中级组) : 

设某个人属于小组 $g$。由于每个小组的座位构成连续区间,所以其他小组相对于 $g$ 只有两种:

- 整个小组位于 $g$ 的左边。
- 整个小组位于 $g$ 的右边。

当这个人进入时,设:

- $L$ 表示已经进入且小组位于 $g$ 左边的人数。
- $R$ 表示已经进入且小组位于 $g$ 右边的人数。

的下界

无论给这个人安排小组内的哪个座位:

- 左边至少有 $L$ 个已入座的人。
- 右边至少有 $R$ 个已入座的人。

所以这个人至少需要跨过 $\min(L,R)$ 个已经有人坐下的座位。

如果 $L\le R$,就让这个人的座位位于所有已经入座的同组成员左边。

此时他的左边没有已经入座的同组成员,因此从左边进入只会跨过 $L$ 个座位。

如果 $L>R$,就让这个人的座位位于所有已经入座的同组成员右边。

此时从右边进入只会跨过 $R$ 个座位。

因此每个人的最优代价都可以独立达到,答案就是 $\sum\min(L, R)$。

对于每个人记录一种选择:

- 若 $L\le R$,记为向同组已有成员的左边插入。
- 否则,记为向右边插入。

对于一个小组,最终的座位顺序为:

1. 所有向左插入的人,按照进入顺序倒序排列。
2. 所有向右插入的人,按照进入顺序正序排列。

例如某组成员依次选择:

右 右 左 左

最终的相对顺序为:

第 4 人 第 3 人 第 1 人 第 2 人

因此只需统计每组有多少人向左插入,就能直接算出每个人的座位。

用树状数组维护各个小组已经进入的人数。

把每个小组的区间左端点作为它在树状数组中的位置。

设小组 $g$ 的区间左端点为 $s_g$:

$L=\operatorname{sum}(s_g-1)$。

而:$R=i-1-\operatorname{sum}(s_g)$。

其中 $i-1$ 是当前已经进入的总人数,$\operatorname{sum}(s_g)$ 包括左侧小组与当前小组已经进入的人。

时间复杂度为:$O(n\log n)$。
 

# 代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6 + 5;
int n, l[N], a[N], c[N], tr[N];
unsigned char d[N];
struct IO {
    static const int S = 1 << 20;
    int p = 0, q = 0;
    char b[S];
    char gc() {
        if (p == q)q = fread(b, 1, S, stdin), p = 0;
        return p == q ? 0 : b[p++];
    }
    int rd() {
        int x = 0;
        char ch = gc();
        while (ch < '0' || ch > '9')ch = gc();
        while (ch >= '0' && ch <= '9')x = x * 10 + ch - '0', ch = gc();
        return x;
    }
} io;
struct OUT {
    static const int S = 1 << 20;
    int p = 0;
    char b[S];
    ~OUT() {
        fl();
    }
    void fl() {
        fwrite(b, 1, p, stdout);
        p = 0;
    }
    void pc(char ch) {
        if (p == S)fl();
        b[p++] = ch;
    }
    void wt(ll x) {
        if (x >= 10) wt(x / 10);
        pc(x % 10 + '0');
    }
} out;
int sum(int x) {
    int s = 0;
    for (; x; x -= x & -x) s += tr[x];
    return s;
}
void add(int x) {
    for (; x <= n; x += x & -x) tr[x]++;
}
int main() {
    n = io.rd();
    for (int i = 1, x; i <= n; i++) {
        x = io.rd();
        if (!l[x])l[x] = i;
    }
    ll ans = 0;
    for (int i = 1, x, L, R; i <= n; i++) {
        x = io.rd();
        a[i] = x;
        L = sum(l[x] - 1);
        R = i - 1 - sum(l[x]);
        if (L <= R) d[i] = 0, c[x]++, ans += L;
        else d[i] = 1, ans += R;
        add(l[x]);
    }
    for (int i = 1; i <= n; i++)
        if (l[i]) c[i] += l[i], l[i] = c[i] - 1;
    out.wt(ans);
    out.pc('\n');
    for (int i = 1, x; i <= n; i++) {
        x = a[i];
        if (!d[i]) a[i] = l[x]--;
        else a[i] = c[x]++;
        out.wt(a[i]);
        out.pc(' ');
    }
    return 0;
}

T4/T2(中级组):

需要利用一个性质:**能走至少 $k$ 个钉子的起点,一定构成一个前缀和一个后缀。 中间已经失效的点以后永远不会重新有效,因此后续不再扫描它们。

同时去掉复制数组的第三遍循环,直接交换两个 DP 数组。

设当前要求还能碰撞 $k$ 个钉子,点 $j$ 对应的最小限制为 $f_j$。

点 $i$ 可以向左走,当且仅当存在 $j<i$ 满足:


$a_j + f_j\le a_i$。
 

只要某个 $i$ 满足,那么所有更靠右的点也满足,所以向左转移可行的点构成一个后缀。

同理,向右转移可行的点构成一个前缀。

因此每一层的有效点都是:


$[1,l]\cup[r, n]$。
 

并且一条长度为 $k + 1$ 的路径删去最后一个点后,就是长度为 $k$ 的路径,所以有效集合只会不断缩小。

# 代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e6 + 5;
const ll I = 4e18;
ll a[N], f0[N], f1[N], v[N];
ll *f = f0, *g = f1;
int s[N], n;
unsigned char ans[N];
struct IO {
    static const int S = 1 << 20;
    int p = 0, l = 0;
    char b[S];
    char gc() {
        if (p == l)l = fread(b, 1, S, stdin), p = 0;
        return p == l ? 0 : b[p++];
    }
    ll rd() {
        ll x = 0;
        char c = gc();
        while (c < '0' || c > '9')c = gc();
        while (c >= '0' && c <= '9')x = x * 10 + c - '0', c = gc();
        return x;
    }
} io;
struct OUT {
    static const int S = 1 << 20;
    int p = 0;
    char b[S];
    ~OUT() {
        fl();
    }
    void fl() {
        fwrite(b, 1, p, stdout);
        p = 0;
    }
    void pc(char c) {
        if (p == S)fl();
        b[p++] = c;
    }
    void wt(int x) {
        if (x >= 10)wt(x / 10);
        pc(x % 10 + '0');
    }
} out;
int main() {
    n = io.rd();
    for (int i = 1; i <= n; i++)a[i] = io.rd();
    if (n == 1) {
        out.wt(1);
        out.pc('\n');
        return 0;
    }
    ll d = I;
    for (int i = 1; i < n; i++)d = min(d, a[i + 1] - a[i]);
    for (int i = 1; i <= n; i++) {
        ll x = I;
        if (i > 1)x = min(x, a[i] - a[i - 1]);
        if (i < n)x = min(x, a[i + 1] - a[i]);
        f[i] = x << 1;
        ans[i] = 2;
    }
    ll z = (a[n] - a[1]) / d;
    int lim = 2 + 63 - __builtin_clzll(z);
    int l = n, r = n + 1;
    for (int k = 3; k <= lim; k++) {
        int t = 0, q = 0, pre = 0;
        for (int o = 0; o < 2; o++) {
            int L = o ? r : 1, R = o ? n : l;
            for (int i = L; i <= R; i++) {
                if (pre) {
                    ll x = a[pre] + f[pre];
                    while (t && v[t] >= x)t--;
                    if (q > t)q = t;
                    s[++t] = pre;
                    v[t] = x;
                }
                while (q < t && v[q + 1] <= a[i])q++;
                g[i] = q ? (a[i] - a[s[q]]) << 1 : I;
                pre = i;
            }
        }
        t = q = pre = 0;
        int nl = 0, nr = n + 1;
        bool suf = 1, ok = 0;
        for (int o = 0; o < 2; o++) {
            int L = o ? l : n, R = o ? 1 : r;
            if (o && l + 1 < r)suf = 0;
            for (int i = L; i >= R; i--) {
                if (pre) {
                    ll x = a[pre] - f[pre];
                    while (t && v[t] <= x)t--;
                    if (q > t)q = t;
                    s[++t] = pre;
                    v[t] = x;
                }
                while (q < t && v[q + 1] >= a[i])q++;
                if (q) {
                    ll x = (a[s[q]] - a[i]) << 1;
                    if (x < g[i])g[i] = x;
                }
                if (g[i] < I) {
                    ans[i] = k;
                    ok = 1;
                    if (suf)nr = i;
                    else if (!nl)nl = i;
                } else suf = 0;
                pre = i;
            }
        }
        if (!ok)break;
        swap(f, g);
        if (nr == 1)l = n, r = n + 1;
        else l = nl, r = nr;
    }
    for (int i = 1; i <= n; i++) {
        out.wt(ans[i]);
        out.pc(' ');
    }
    return 0;
}

时间复杂度为 $O\left(\sum_k|S_k|\right)$,
 

其中 $S_k$ 是能够碰撞至少 $k$ 个钉子的起点集合。

T3(中级组):

## 思路

先考虑一次释放复仇之魂能做什么。

若当前要击杀第 $i$ 个怪物,设它的位置为 $q_i$,其中 $q$ 为 $p$ 的逆排列。

向左释放或向右释放,本质上要求后续被杀怪物的位置单调。因此可以预处理最长单调段,求出从每个怪物开始释放最多能连续击杀到哪里。

于是每次释放对应一个区间 $[l,r]$ 表示从第 $l$ 个怪物开始释放,可以一次杀到第 $r$ 个怪物。

---

如果全部使用骨针攻击,需要攻击 $n$ 次。

对于一个区间 $[l,r]$,释放复仇之魂可以减少:$$r-l$$ 次攻击。

但是释放一次需要消耗 $1$ 点灵魂,而少打 $x$ 次骨针会使之后可用的灵魂少 $x+1$ 点,所以一个收益为 $x$ 的方案实际占用 $x+2$点容量。

因此问题转化为:

> 有若干个任务,每个任务有截止时间 $r$。选择若干任务,使总收益最大,并满足所有前缀中的总占用容量不超过截止时间。

---

按照右端点从小到大处理任务。

维护当前选择的任务:

- 总收益。
- 总占用容量。
- 一个小根堆,存当前收益最小的任务。

当加入一个新区间导致容量超过限制时:

- 如果删除收益最小的任务可以解决超限,就删除它。
- 如果只需要减少部分容量,就缩短该任务的收益。

由于每次删除收益最小的任务一定最优,因此可以通过贪心得到最大收益。

最后答案为:

$$
n-\text{最大减少的攻击次数}
$$。

总时间复杂度为 $O(n\log n)$

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6 + 5;
int n, p[N], q[N], a[N], b[N], u[N], d[N], f[N];
int m, hs, h[N], ps[N], y[N];
bool z[N], t[N];
int ky(int x) {
	return y[x] - (x < m && t[x + 1] && z[x + 1]);
}
bool cp(int x, int v) {
	int a = ky(x), b = ky(v);
	return a < b || (a == b && x > v);
}
void sw(int x, int v) {
	swap(h[x], h[v]);
	ps[h[x]] = x;
	ps[h[v]] = v;
}
void up(int x) {
	while (x > 1 && cp(h[x], h[x >> 1])) sw(x, x >> 1), x >>= 1;
}
void dn(int x) {
	while (1) {
		int v = x, l = x << 1, r = l | 1;
		if (l <= hs && cp(h[l], h[v])) v = l;
		if (r <= hs && cp(h[r], h[v])) v = r;
		if (v == x) return;
		sw(x, v);
		x = v;
	}
}
void ins(int x) {
	h[++hs] = x;
	ps[x] = hs;
	up(hs);
}
void fix(int x) {
	if (!ps[x]) return;
	int v = ps[x];
	up(v);
	dn(ps[x]);
}
int pop() {
	int x = h[1];
	ps[x] = 0;
	if (hs == 1) {
		hs = 0;
		return x;
	}
	h[1] = h[hs--];
	ps[h[1]] = 1;
	dn(1);
	return x;
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin >> n;
	for (int i = 1; i <= n; i++) cin >> p[i], q[p[i]] = i;
	for (int i = 1; i <= n; i++) a[i] = max(a[i - 1], p[i]);
	for (int i = n; i >= 1; i--) b[i] = max(b[i + 1], p[i]);
	u[n] = d[n] = n;
	for (int i = n - 1; i >= 1; i--) {
		u[i] = q[i] < q[i + 1] ? u[i + 1] : i;
		d[i] = q[i] > q[i + 1] ? d[i + 1] : i;
	}
	for (int i = 1; i <= n; i++) {
		int r = a[q[i]];
		if (r > i && r <= d[i]) f[i] = r;
		r = b[q[i]];
		if (r > i && r <= u[i]) f[i] = r;
	}
	int i = 1, lr = -1;
	ll sm = 0, ans = 0;
	while (i <= n) {
		if (!f[i]) {
			i++;
			continue;
		}
		int l = i, r = f[i], w = r - l;
		++m;
		t[m] = m > 1 && l == lr;
		int v = w - (t[m] && z[m - 1]);
		if (v > 0) {
			z[m] = 1;
			y[m] = v;
			sm += v + 2;
			ans += v;
			ins(m);
			if (t[m] && z[m - 1]) fix(m - 1);
		}
		while (hs) {
			int x = h[1], k = ky(x);
			if (k || sm > r) {
				if (k && sm <= r) break;
				ll e = sm - r;
				if (k && e < k) {
					y[x] -= e;
					sm -= e;
					ans -= e;
					fix(x);
					break;
				}
			}
			x = pop();
			bool nx = x < m && t[x + 1] && z[x + 1];
			bool pr = x > 1 && t[x] && z[x - 1];
			int v = y[x];
			z[x] = 0;
			y[x] = 0;
			sm -= v + 2;
			ans -= v;
			if (nx) {
				y[x + 1]++;
				sm++;
				ans++;
				fix(x + 1);
			}
			if (pr) fix(x - 1);
		}
		lr = r;
		i = r;
	}
	cout << n - ans << '\n';
}

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐