【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
开题 + 补题情况
还是很吃教训的一场比赛,被博弈论硬控两小时(很好的一个博弈论题),dijkstra被卡map,最终三题。
总结
上百通过的题已补完,还是学到了很多东西,其实这些题目也不是自己不会,往往是题目信息转化的能力不足(1002 和 1004),或是赛时被卡题没有及时换题导致会的题没有开出(1009),又或是复杂度分析能力不足(1004被卡常),补完题还是学到了很多东西,有很多之前自己没有注意过的地方吃了教训。
自己不会的东西还是太多了,还得多多练习多多长见识,革命尚未结束,加油!
1001 - 签到
题如其名,签到题,问给出的字符串第一次出现的位置。
点击查看代码
#define int long long
void solve()
{
int n;cin >> n;
string s;cin >> s;
bool ck = false;
for(int i = 1;i <= n;i ++) {
string t;cin >> t;
if(s == t) {
cout << i << '\n';
ck = true;
}
}
if(!ck)cout << -1 << '\n';
}
1006 - 密码
移项可得:\(x = (c - b) / a\),那么只要枚举一下 \(u, v, w\) 的两两组合,组合起来的作差取绝对值,剩下那一个也取绝对值,若能整除则添加贡献,对于求得的所有值,贡献为 \(n\) 的就是答案(注意对于每一组 \(u, v, w\) 要去重一下只能算一次贡献,否则难以计数)。
点击查看代码
#define int long long
void solve()
{
map<int, int> mp;
int n;cin >> n;
for(int i = 1;i <= n;i ++)cin >> a[i] >> b[i] >> c[i];
set<int> st;
for(int i = 1;i <= n;i ++) {
st.clear();
if(abs(a[i] - b[i]) % abs(c[i]) == 0) {
st.insert(abs(a[i] - b[i]) / abs(c[i]));
}
if(abs(a[i] - c[i]) % abs(b[i]) == 0) {
st.insert(abs(a[i] - c[i]) / abs(b[i]));
}
if(abs(b[i] - c[i]) % abs(a[i]) == 0) {
st.insert(abs(b[i] - c[i]) / abs(a[i]));
}
for(auto &i : st) {
mp[i] ++;
}
}
for(auto &[u, v] : mp) {
if(v == n) {
cout << u << '\n';
break;
}
}
}
1007 - 分配宝藏
很好的一道博弈论题,之前遇到的博弈论,都是两个人博弈,分析必胜态和必败态就行了。
这里我们要分析的就不是必胜态和必败态了,而是 \(n\) 个人的诉求。
关于这 \(n\) 个人的诉求,题目有一句话很凝练:保证自己不被杀死的前提下企图获得更大的利益。
也就是说,既要保全自己,又要尽可能让自己有尽可能多的钱。
也就是说,对于船长,要尽可能给少,对于其他人,要尽可能拿多。
对于此题,我们可以用类似两人博弈的思路来思考,从结局往回推。
我们将所有船员从后往前编号为 \(1, 2, 3, ..., n\)。
对于 \(n = 1\),此时无论如何染染一定不会被杀,因此不用给钱,分到的钱为 \(0\)。
对于 \(n = 2\),我们必须贿赂一个船员举手不杀染染,才能保全染染,那对于 \(1\) 号船员和 \(2\) 号船员,贿赂谁呢?
注意到,如果染染被杀,此时人数变为 \(1\),那么此时的船长就会按照 \(n = 1\) 时的最优策略来分配宝藏,也就是 \(1\) 号船员会分到 \(0\) 元,我们死了过后,\(1\) 号船员无法获得金币,因此,我们给他 \(1\) 个金币,对他来说就是赚的,那他也一定不会杀掉染染,此时过半,染染存活,而对于 \(2\) 号船员,如果染染死掉,他会成为船长,他可以随意选择给自己留多少金币,因此就算贿赂了他他也会杀掉染染,因此不能贿赂 \(2\) 号船员,因此 \(n = 2\) 时的答案为 \(0,1\)。
我们继续按上述思路分析,当 \(n = 3\) 时,如果染染被杀,此时人数变为 \(2\),那么此时的船长就会按照 \(n = 2\) 时的最优策略来分配宝藏,也就是 \(0,1\),那么回到 \(n = 3\) 的情况,如果染染被杀,\(2\) 号船员无法获得金币,因此,我们给他 \(1\) 个金币,对他来说就是赚的,那他也一定不会杀掉染染,而对于 \(1\) 号船员,如果染染被杀,他仍能获得一个金币,若要贿赂他需要花费 \(2\) 金币,不优,因此不能贿赂 \(1\) 号船员,对于 \(3\) 号船员,如果染染死掉,他会成为船长,他可以随意选择给自己留多少金币,因此就算贿赂了他他也会杀掉染染,因此不能贿赂 \(3\) 号船员,因此 \(n = 3\) 时的答案为 \(0,1,0\)。
按这个逻辑推下去我们会发现,其实我们要贿赂的,就是染染死掉之后,得不到金币的,此时只需要花费一个金币就能贿赂到他不杀自己,并且这个数量加上染染后一定会过半(可以观察发现是 \(0,1\) 交替,也可以通过分析染染贿赂的人数少了过后对下一位船长的决策优越性的影响来证明这个事情,见下方),否则对于染染死后下一个船长的最优解会不成立,所以答案就构造出来了,可以发现获得金币为 \(1\) 的船员的序号序列就是一个公差为 \(2\) 的等差数列,只需要找好首项和末项即可。
简单证明一下:首先无论奇偶,需要贿赂的人都是 \(\lfloor n / 2 \rfloor\)。
- 若 \(n\) 是奇数,则 \(\lfloor n / 2 \rfloor = \lfloor (n - 1) / 2 \rfloor\),那么如果这一轮染染贿赂到的人少于 \(\lfloor n / 2 \rfloor\),那么说明染染死后下一位船长贿赂到的人就多于 \(\lfloor n / 2 \rfloor\),对下一位船长来说这个结果一定不优。
- 若 \(n\) 是偶数,则 \(\lfloor n / 2 \rfloor = \lfloor (n - 1) / 2 \rfloor + 1\),由上面的分析可得,染染一定不会贿赂下一位船长,因此染染的贿赂考虑人数范围实际上是 \(n - 1\) 人,这与下一位船长之后的人数相同,在这些人里面,如果染染少贿赂一个,那么说明下一位船长就多贿赂了一个,对下一位船长来说这个结果一定不优。
再仔细复盘一下,其实这个和两个人的博弈是类似的,两个人的博弈,会存在一个必胜态和必败态的转化,每个人的行为都是为了让自己获胜,那在这个题里面,对于每一个人单独分析,也就存在一个杀掉船长后,自己是获利还是亏损,也就是题目说的“更大的利益”,要不要杀当前的船长,自然也就出来了,并且对于每一个船长,为了保全自己,也就是题目说的“保证自己不被杀死”,自然也就会选择最优选择,那么杀掉染染之后的分金币情况也就出来了。
红温记录:
点击查看代码(省略了逆元)
#define int long long
void solve()
{
int n;cin >> n;
int sum = n / 2;
int ans = 0;
if(n & 1)n --;
ans = (n + n - (sum - 1) * 2) % M * sum % M * inv(2);
cout << (ans % M + M) % M << '\n';
}
1005 - 航线
很明显的dijkstra求最短路,博弈论做不出来的时候十分钟就出思路了,但是!!被卡map了...有亿点点无语啊啊啊!!!
做这个题需要明确一点,dijkstra的本质是数据结构优化DP,因此状态很重要(当然也可以提前建边建图),每一个点的四个方向作为dijkstra每个点的四种状态!然后用dijkstra进行转向和移动就行,具体看代码吧,一看就懂,但是被卡map真的很难受啊啊啊啊QWQ,以后能开数组绝不用map了。
点击查看代码
#define int long long
struct Node {
int x, y, t, di;
bool operator < (const Node &v) const {
return t > v.t;
}
};
int n, m;
bool inmp(int x, int y) {
return x >= 1 && x <= n && y >= 1 && y <= m;
}
int dijk(vector<vector<int>> &t, vector<vector<int>> &d) {
priority_queue<Node> pq;
vector<vector<vector<int>>> vis(n + 2, vector<vector<int>>(m + 2, vector<int>(4, 0)));
pq.push({1, 1, t[1][1], 1});
while(pq.size()) {
auto [x, y, pret, predi] = pq.top();
pq.pop();
if(x == n && y == m && predi == 0) {
return pret;
}
if(vis[x][y][predi])continue;
vis[x][y][predi] = true;
pq.push({x, y, pret + d[x][y], (predi + 1) % 4});
pq.push({x, y, pret + d[x][y], (predi + 2) % 4});
pq.push({x, y, pret + d[x][y], (predi + 3) % 4});
if(predi == 0 && inmp(x + 1, y)) {
pq.push({x + 1, y, pret + t[x + 1][y], predi});
}
if(predi == 1 && inmp(x, y + 1)) {
pq.push({x, y + 1, pret + t[x][y + 1], predi});
}
if(predi == 2 && inmp(x - 1, y)) {
pq.push({x - 1, y, pret + t[x - 1][y], predi});
}
if(predi == 3 && inmp(x, y - 1)) {
pq.push({x, y - 1, pret + t[x][y - 1], predi});
}
}
}
void solve()
{
cin >> n >> m;
vector<vector<int> > t(n + 2, vector<int>(m + 2, 0)), d(n + 2, vector<int>(m + 2, 0));
for(int i = 1;i <= n;i ++) {
for(int j = 1;j <= m;j ++) {
cin >> t[i][j];
}
}
for(int i = 1;i <= n;i ++) {
for(int j = 1;j <= m;j ++) {
cin >> d[i][j];
}
}
cout << dijk(t, d) << '\n';
}
1002 - 船长(补题)
这个题题意很简单,就是从前到后,相邻两个人对抗,决出胜负后剩下的人继续相邻两人对抗,如果总人数为奇数,则最后一个人本轮直接晋级,染染对抗任何人都获胜,其他人获胜的概率均为 \(1 / 2\),有 \(k\) 个人是对染染有威胁的人,问染染有多大的概率不碰上对他有威胁的人。
仔细观察可以发现,这个对抗的关系实际上是一个树形的结构,两两对抗,赢的人继续两两对抗,那么对于相邻两个人,向上连接同一个结点,代表这两人中获胜的那一个,如果是最后一个落单的,就直接向上连一个结点,这么进行下去,就会形成一棵不完全的二叉树结构了。
记 \(f_i\) 为结点 \(i\) 是对染染有威胁的人的概率,那么按照概率论的知识,转移方程也很明显,当两个非染染结点 \(i\) 和 \(j\) 对抗的时候,对抗后得到的胜者结点为 \(k\),有 \(f_k = (f_i + f_j) / 2\),如果结点 \(i\)是落单的,则有 \(f_k = f_i\)。
如果结点 \(i\) 和 \(j\) 对抗的时候,结点 \(i\) 是染染,由于在比赛之初,对染染有威胁的人可能在第几轮遇到染染就是已经定下来的了(因为始终是两两相邻组合,这个组合方式不会因谁胜谁负而改变),所以不同层对染染有威胁的人遇到染染是相互独立的,因此可以按照乘法原理就有 \(ans = ans \times (1 - f_j)\)。
但是我们题目中的总人数 \(n\) 是 \(10^9\) 级别的,因此肯定不可能实际地建一棵树,但是,对染染有威胁的人数 \(k\) 的级别是 \(10^5\) 的,因此可以从这方面入手。我们知道,如果两个相互对抗的人都对染染没有威胁,那么传递上去的概率仍然是 \(0\),因此这些我们根本不需要去计算,而只需要离散化到 \(k\) 个对染染有威胁的人进行模拟对抗和爬树即可。
那么如何模拟呢?我们将所有人以 \(0\) 为起始编号排序,可以发现,相邻两个对抗的人 \(i,j\),有 \(i / 2 = j / 2\),并且 \(i / 2\) 就可以作为获胜者在下一轮的编号,这样所有人的编号在每一轮都会是连续的。
因此遍历威胁的人的数组 \(p\),染染在每一轮的编号为 \(ix\),分以下四种情况讨论(此处省略 \(i + 1 < n\) 相关的边界判断,代码里面有):
- 如果 \((p_i / 2) = (ix / 2)\),说明这个人此轮与染染战斗,那么计算答案即可 \(ans = ans \times (1 - f_i)\)。
- 如果 \((p_i / 2) = (p_{i + 1} / 2)\),说明这相邻两个有威胁的人此时相互战斗,那么计算他们获胜者是有威胁的人的概率 \((f_i + f_{i + 1}) / 2\)。
- 如果 \((p_i / 2) \neq (p_{i + 1} / 2)\),并且 \(p_i \oplus 1 < n\),说明 \(p_i\) 有战斗的人,并且另一个人一定没有威胁,那么计算 \(p_i\) 和他另一个一定没有威胁的人战斗后获胜者是有威胁的人的概率 \(f_i / 2\)。
- 如果 \((p_i / 2) \neq (p_{i + 1} / 2)\),并且 \(p_i \oplus 1 >= n\),说明 \(p_i\) 是落单的人,直接晋级,晋级后的这个人是有威胁的人的概率为 \(f_i\)。
对于后三种情况,我们开一个临时数组 \(g\) 来表示获胜的人是有威胁的人的概率,再用一个临时数组 \(q\) 来存储获胜的人的编号,在本轮结束后,进行 \(p = q, f = g\) 迭代操作,并且 \(n = (n + 1) / 2\),因为胜出的人是当前人数的一半向上取整,以及 \(ix = ix / 2\),因为染染在下一轮的编号也要减半,然后进入下一轮循环,一直迭代,直到 \(p\) 的大小为 \(0\),也就是染染获胜。
本题的思路部分就到此结束了,确实是一个很好的题,但本人在补题过程中被疯狂卡常,遂总结于此:
- 本题多处使用了 \(inv(2)\) 这个逆元值,若每次都用快速幂求逆元,则会因为复杂度多一个 \(\log\) 而超时,因此,应该提前计算 \(inv(2)\) 存储下来,降低计算时的复杂度。
点击查看代码
const int N = 2e5 + 9, M = 998244353;
int inv2;
void solve()
{
int n, k;std::cin >> n >> k;
std::vector<int> p(k), f(k, 1);
int ix;std::cin >> ix;
ix --;
for(int i = 0;i < k;i ++) {
std::cin >> p[i];
p[i] --;
}
std::sort(p.begin(), p.end());
int ans = 1;
while(p.size()) {
std::vector<int> q, g;
for(int i = 0;i < p.size();) {
if((ix >> 1) == (p[i] >> 1)) {
ans = 1ll * (1ll * ans * ((1ll + M - f[i] + M) % M)) % M;
i ++;
} else if(i + 1 < p.size() && ((p[i] >> 1) == (p[i + 1] >> 1))) {
q.push_back(p[i] >> 1);
g.push_back(1ll * (f[i] + f[i + 1]) % M * inv2 % M);
i += 2;
} else if((p[i] ^ 1) >= n) {
q.push_back(p[i] >> 1ll);
g.push_back(f[i] % M);
i ++;
} else {
q.push_back(p[i] >> 1ll);
g.push_back(1ll * f[i] * inv2 % M);
i ++;
}
}
p = q;
f = g;
n = (n + 1) >> 1;
ix >>= 1;
}
std::cout << (ans % M + M) % M << '\n';
}
signed main()
{
std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
inv2 = (M + 1) / 2;
int t = 1;std::cin >> t;
while(t --)solve();
return 0;
}
1004 - 海浪(补题)
题目对于海浪的定义是,在一个区间内,存在一个海面实数高度,使得区间内任何两个相邻海面,一个高于该高度,一个低于该高度。
比如,\(1,2,1\) 是符合题目要求的海浪,而 \(1,2,3\) 就不是题目要求的海浪。
可以看出相邻两个海面的下标,一个是奇数,一个是偶数,并且一定是奇偶相间的,除了边界外,一个奇数会匹配两个偶数,一个偶数会匹配两个奇数。
要让一个区间是海浪,若 \(a_i < a_{i + 1}\),则 \(a_{i + 2} < a_{i + 1}\),则 \(a_{i + 3} > a_{i + 2}\),并且 \(a_{i + 3} > a_{i}\),由此推下去可以发现,从这个起始条件出发,可以得出结论,要让一个区间是海浪,则这个区间偶数位置的最小值,要大于这个区间奇数位置的最大值。
若起始条件相反,\(a_i > a_{i + 1}\),也可以得出相应的结论,要让一个区间是海浪,则这个区间偶数位置的最大值,要小于这个区间奇数位置的最小值。
我们对第一种情况进行考虑,对于第二种情况,我们只需要把所有海面高度取反,就转化为了第一种情况。
由于一个海浪区间是连续的,因此我们可以使用双指针算法来寻找每一个左端点对应的海浪最远右端点,用两个优先队列,一个大根堆存储奇数下标,一个小根堆存储偶数下标,在添加的过程中,只需要根据新加进来的位置是奇数还是偶数从而和偶数最小值还是奇数最大值进行比较看能不能加进来即可,并用滑动窗口的手法移除掉不合法的堆顶元素。
处理好了每一个下标对应的海浪最远右端点后,我们再用一个 ST 表来存储区间内的左端点的最大海浪宽度。
然后就可以处理查询了,对于每一个查询 \([x, y]\),由于我们存储的所有信息都是对于左端点来记录的,所以我们对右端点进行分类讨论:
- 当右端点 \(> y\),这种情况对应的左端点一定是有单调性的,因为海浪是连续的,一旦有一个左端点不满足对应的右端点 \(> y\),那么它左边的其他点也都不满足。
- 当 \(x \leq\) 右端点 \(\leq y\),此时我们已经把上述 \(> y\) 的情况剖除掉了,假设上面的情况最左端点为 \(e\),则对于 \([x, e - 1]\),他们的海浪区间一定都在 \([x, y]\) 内,此时只需要在 \([x, e - 1]\) 中查询 ST 表的最大值即可。
对这两种情况找到的最大值再取一个最大值,就是这个查询区间的最大值了。
时间复杂度:\(O((n + q) \log n)\)
点击查看代码
#define int long long
const int N = 1e5 + 9, M = 1e9 + 7;
int a[N];
int r[N];
int st[N][20];
int ans[N];
int log_2[N];
int n, q;
struct myoddpq {
bool operator () (const int &u, const int &v) const {
return a[u] < a[v];
}
};
struct myevenpq {
bool operator () (const int &u, const int &v) const {
return a[u] > a[v];
}
};
void getr() {
std::priority_queue<int, std::vector<int>, myoddpq> odd;
std::priority_queue<int, std::vector<int>, myevenpq> eve;
for(int i = 1, j = 0;i <= n;i ++) {
while(j + 1 <= n) {
while(eve.size() && eve.top() < i)eve.pop();
while(odd.size() && odd.top() < i)odd.pop();
if((j + 1) & 1) {
if(eve.size()) {
if(a[j + 1] < a[eve.top()]) {
j ++;
odd.emplace(j);
} else {
break;
}
} else {
j ++;
odd.emplace(j);
}
} else {
if(odd.size()) {
if(a[j + 1] > a[odd.top()]) {
j ++;
eve.emplace(j);
} else {
break;
}
} else {
j ++;
eve.emplace(j);
}
}
}
r[i] = std::max(r[i], j);
}
}
void getst() {
int lg = ceill(log2l(n));
for(int i = 1;i <= n;i ++) {
st[i][0] = r[i] - i + 1;
}
for(int k = 1;k <= lg;k ++) {
for(int i = 1;i + (1 << (k - 1)) <= n;i ++) {
st[i][k] = std::max(st[i][k - 1], st[i + (1 << (k - 1))][k - 1]);
}
}
log_2[1] = 0;
for(int i = 2;i <= n;i ++) {
log_2[i] = log_2[i / 2] + 1;
}
}
int query(int l, int r) {
if(l > r)return 0;
int s = log_2[r - l + 1];
return std::max(st[l][s], st[r - (1 << s) + 1][s]);
}
void init() {
std::cin >> n >> q;
for(int i = 1;i <= n;i ++) {
std::cin >> a[i];
}
for(int i = 1;i <= n;i ++) {
r[i] = i;
}
for(int i = 1;i <= q;i ++) {
ans[i] = 1;
}
}
void solve()
{
init();
getr();
for(int i = 1;i <= n;i ++) {
a[i] = -a[i];
}
getr();
getst();
for(int i = 1;i <= q;i ++) {
int x, y;std::cin >> x >> y;
int s = x - 1, e = y + 1;
while(s + 1 != e) {
int mid = s + e >> 1;
if(r[mid] > y)e = mid;
else s = mid;
}
ans[i] = std::max(ans[i], y - e + 1);
ans[i] = std::max(ans[i], query(x, e - 1));
}
int res = 0;
for(int i = 1;i <= q;i ++) {
res = (res + ans[i] * i % M) % M;
}
std::cout << (res % M + M) % M << '\n';
}
1009 - 切割木材(补题 和题解做法不太一样)
有点后悔赛时因为被卡题没看这个题了。
没看题解补的题,很典的模型,之前省赛遇到过一样的模型,当时没有做出来,这次十几分钟就出思路了,但是因为自己有点唐调了一会。
和题解的思路不太一样,我使用的是线段树优化 DP。
首先,将一个不允许重新排序的序列分成连续的很多区间,求某个东西的最值或其他信息,这是很典型的一个 DP 模型,记 \(dp_i\) 为到第 \(i\) 个数字为止的分割方案的最大值。
那么很容易就能想到一个暴力思路:\(dp_i = \max(dp_j + g(f(j + 1, i))),j \in [1, i - 1]\),同样很明显,这个做法的时间复杂度为 \(O(n ^ 2)\),而我们的 \(n \leq 10^5\),因此无法通过此题。
当某个思路复杂度无法通过时,先不要否认此做法,而是观察能否优化。
我们看一下题目给出的 \(f(l, r)\) 的定义:对于区间 \([l, r]\) 中的数,对二进制中每一位分别讨论,如果这个区间中的数字在这一位上既有 \(1\) 又有 \(0\),那么 \(f(l, r)\) 这一位就是 \(1\),否则是 \(0\)。
那么再回到我们上面的状态转移方程,我们以 \(i\) 为起点,向左合并,一旦 \(a_j\) 的某一位和 \(a_i\) 不同,\(f(j, i)\) 的值就会加上这一位为 \(1\) 的贡献,并且在后面一定不会再变成 \(0\),那不就是在向左合并的过程中,\(f(j, i)\) 的值具有单调性吗,题目中 \(m \leq 20\) 那么我们向左合并的过程中 \(f(j, i)\) 最多变化 \(m\) 次,那么如果我们把 \(f(j + 1, i)\) 相同的区间直接找一个最大的 \(dp_j\) 用于转移,那么最多向左合并 \(m\) 次,那么我们不就可以在 \(O(m)\) 的次数找出 \(dp_i\) 的值了吗?
我们使用数组 \(l_{0/1,k}\) 来表示第 \(k\) 位为 \(0 / 1\) 的数在 \(i\) 之前出现的最靠近 \(i\) 的位置,我们用 \([s, e]\) 来表示有着相同 \(f(j + 1, i)\) 的一个区间,用 \(tmp\) 来表示当前的 \(f(j + 1, i)\),那么初始的 \(e\) 就是 \(i - 1\),初始的 \(tmp\) 就是 \(0\)(因为初始只有 \(a_i\) 一个数),此时在 \(\leq e\) 的范围中找到第一个和 \(a_i\) 有不同位的数,并且这一位在之前没有出现过,记为 \(s\),那么对于 \([s + 1, e + 1]\) 这个范围,\(tmp\) 的值都不会发生变化,那么记 \(mx = \max(dp_j), j \in [s, e]\),状态转移方程就可以写为 \(dp_i = \max(dp_i, mx + tmp)\),然后,\(e = s - 1\) 继续迭代计算,直到 \(s = 0\)。对于 \(mx\),由于需要及时修改,我们使用线段树来维护,在每次计算出 \(dp_i\) 后,将 \(dp_i\) 的值加入线段树,并更新 \(l\) 数组。
总时间复杂度 \(O(n \times m ^ 2 \times \log n)\),题目给出了 3s ,足以通过此题。
具体实现详见代码。
点击查看代码
#include <bits/stdc++.h>
#define inf 2e18
#define ull unsigned long long
#define int long long
#define ls o << 1
#define rs o << 1 | 1
const int N = 1e5 + 9;
int a[N];
int g[1 << 20];
int l[2][20];
int dp[N];
struct segtree {
int t[N << 2];
int s[N << 2];
int e[N << 2];
void build(int l, int r, int o = 1) {
s[o] = l;
e[o] = r;
if(l == r) {
t[o] = (l == 0) ? 0 : -inf;
return;
}
int mid = s[o] + e[o] >> 1;
build(l, mid, ls);
build(mid + 1, r, rs);
pushup(o);
}
void change(int ix, int x, int o = 1) {
if(s[o] == e[o]) {
t[o] = x;
return;
}
int mid = s[o] + e[o] >> 1;
if(ix <= mid)change(ix, x, ls);
else change(ix, x, rs);
pushup(o);
}
int query(int l, int r, int o = 1) {
if(l <= s[o] && e[o] <= r) {
return t[o];
}
int res = -inf;
int mid = s[o] + e[o] >> 1;
if(mid >= l)res = std::max(res, query(l, r, ls));
if(mid + 1 <= r)res = std::max(res, query(l, r, rs));
return res;
}
void pushup(int o) {
t[o] = std::max(t[ls], t[rs]);
}
}sgt;
void init(int n, int m) {
for(int i = 1;i <= n;i ++)std::cin >> a[i];
for(int i = 0;i < (1 << m);i ++)std::cin >> g[i];
for(int i = 0;i < m;i ++) {
l[1][i] = l[0][i] = 0;
}
dp[0] = 0;
for(int i = 1;i <= n;i ++) {
dp[i] = -inf;
}
}
void solve()
{
int n, m;std::cin >> n >> m;
init(n, m);
sgt.build(0, n);
for(int i = 1;i <= n;i ++) {
int tmp = 0;
int s, e = i - 1;
while(true) {
s = 0;
for(int k = 0;k < m;k ++) {
int now = ((a[i] >> k) & 1);
if(l[now ^ 1][k] > s && l[now ^ 1][k] <= e) {
s = l[now ^ 1][k];
}
}
dp[i] = std::max(dp[i], sgt.query(s, e) + g[tmp]);
if(s == 0)break;
e = s - 1;
for(int k = 0;k < m;k ++) {
int now = ((a[i] >> k) & 1);
if(l[now ^ 1][k] == s) {
tmp |= (1 << k);
}
}
}
sgt.change(i, dp[i]);
for(int j = 0;j < m;j ++) {
l[(a[i] >> j) & 1][j] = i;
}
}
std::cout << dp[n] << '\n';
}
作者: 天天超方的
出处: https://www.cnblogs.com/TianTianChaoFangDe
关于作者:ACMer,算法竞赛爱好者
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显醒目位置给出, 原文链接 如有问题, 可邮件(1005333612@qq.com)咨询.