The 2022 ICPC Asia Xian Regional Contest(E,L)题解
E | Find Maximum |
题意:
首先,通过观察与打表,可以发现:
规律:
对于非负整数 x,函数 f(x) 的值等于:
将 xx 写成三进制后,各个位数的数字之和 + 该三进制数的位数。
例如,
-
x=1002(3),则有 f(1002(3))=(1+0+0+2)+4=7。
最大化策略:
由于 f(x) 的值为「位数之和 + 位数」,为了尽可能让 f(x)最大,我们需要:
-
尽可能多地让每个位为数字 2,因为 2 是三进制单个位的最大贡献。
因此问题转化为:
如何在给定区间 [l,r] 中,找到一个三进制表示的数字,使它尽可能地填满数字 2?
具体策略分析:
分情况讨论:
① 若 l 与 r 位数相同:
这种情况下,我们从高位往低位遍历:
-
找到从最高位开始第一个 r与 l 不相同的位。
-
将这一位上的数字(对应的是 r 上的数字)减去 1(此处保证减1后仍然大于或等于 l)。
-
将减1之后的这一位后面的所有位(低位)全部填充为数字 2。
可以证明这是最佳策略,举个具体例子:
-
假设给定 l=120000, r=130000:
-
三进制表示:
-
l=01112120002(3)
-
r=11222012102(3)
-
-
第一个不同位是最高位的
1
与0
的位置。
我们对 r 的最高位1
进行减1操作,使其变为0
。 -
此时前面所有的低位均可填充数字 2,于是我们选出数字:
22222222002
-
对应的 f(x)为各位数字之和(2×8+0+0+2=18)+ 位数(11)=29,是最大值。
-
② 若 r 位数比 l 多:
在此情况下,我们不一定只是简单地对最高位做减1操作,原因是:
-
如果 r 的最高位本身是
1
,减1后变成0
,最高位少了一个 2 的贡献,反而降低了总贡献。 -
这种时候应该对 从最高位到最低的每个比 l 多出来的位数分别做一次减1操作,并将低位填满数字 2,逐个进行判断并取最大值。
举例说明:
-
假设 l=1,r=15(10)=120(3)
-
最高位直接减1为:
220
,贡献为 (2+2+0)+3=7。 -
若不小心减去最高位变为
0
,比如变为021
,贡献仅为 (0+2+1)+3=6,反而更小。
-
-
因此,对于这种情况需要逐个测试所有可能的位置(多出来的位),并选最大值。
③ 特殊情况(l=r):
-
如果 l=r,则答案直接为 f(l)。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e6 + 10;
const int mod = 1e9 + 7;
#define pii pair<int, int>
#define lowbit(x) (x & (-x))
int f(int x)
{if (x == 0)return 1;if (x > 0 && x % 3 == 0)return f(x / 3) + 1;if (x > 0 && x % 3 != 0)return f(x - 1) + 1;
}
vector<int> get(int x)
{vector<int> v;while (x){v.push_back(x % 3);x /= 3;}return v;
}
int l, r;
void solve()
{cin >> l >> r;vector<int> v1 = get(l);vector<int> v2 = get(r);if (v2.size() > v1.size()){int res = 3 * (v2.size() - 1) + (v2.back() - 1) + (v2.back() != 1);res = max(res, f(r));int sum = v2.back() + 1;for (int i = v2.size() - 2; i >= max((int)v1.size() - 1ll, 1ll); i--){if (v2[i] >= 1)res = max(res, sum + i * 3 + v2[i]);}cout << res << endl;return;}int res = 0;for (int i = v2.size() - 1; i >= 0; i--){if (v2[i] == v1[i])res += v2[i] + 1;else{res += 3 * i + v2[i];break;}}res = max(res, f(r));cout << res << endl;
}
signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);int t = 1;cin >> t;while (t--)solve();
}
L | Tree |
题意:
怎么操作
-
反链(条件2)操作:
我们希望每次将最多的节点放入一个集合中。显然,将当前树中所有叶子节点组成一个集合,是最优的做法。 -
链状(条件1)操作:
依然选择所有的叶子节点,每个叶子节点往上会形成一个链,我们可以选这些链进行1操作。
综上所述:
- 反链(条件2)操作,每次只需增加1个集合(所有叶子同时处理)。
- 链状(条件1)操作,每个叶子节点都要单独形成一个集合(增加集合数等于当前叶子节点个数)。
因此,从全局最优的角度看,明显条件2(反链)是我们更倾向于多次重复的操作,而条件1(链状)适合最后进行一次收尾。
基于以上的分析,我们确定最佳策略:
- 从树的叶子节点开始,每次进行一次**反链(条件2)**操作(将所有当前叶子放入同一个集合)。
- 每次执行完反链操作后,将这些叶子节点删除,得到一个更小的树,再次重复上述步骤。
- 在任意一次反链操作结束后,我们都可以选择结束此过程,转而执行一次链状(条件1)操作来处理当前树剩余的所有叶子节点。
换句话说,整个过程是:
- 反复地进行条件2操作(每次集合数+1);
- 在任何时刻可以决定不再进行条件2,改用条件1进行一次性收尾(集合数增加当前树中剩余叶子节点个数);
- 我们尝试所有可能的停止时刻,取其中的最小值即可。
从叶子节点开始跑拓扑排序即可
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e6 + 10;
const int mod = 1e9 + 7;
#define pii pair<int, int>
#define lowbit(x) (x & (-x))
void solve()
{int n;cin >> n;vector<int> in(n + 10, 0);vector<int> fa(n + 10, 0);in[0] = 1e9;for (int i = 2; i <= n; i++){int x;cin >> x;fa[i] = x, in[x]++;}queue<int> q;for (int i = 2; i <= n; i++)if (in[i] == 0)q.push(i);int cnt = 0;int res = n;while (!q.empty()){res = min(res, cnt + (int)q.size());cnt++;int sz = q.size();while (sz--){int t = q.front();q.pop();int x = fa[t];in[x]--;if (in[x] == 0)q.push(x);}}cout << res << endl;
}
signed main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);int t = 1;cin >> t;while (t--)solve();
}