有趣杂题收录

序列局部加减操作:研究关系而非值

\(a_i\leftarrow f(a_{i-1},a_i,a_{i+1})\)
考虑算:\(a_i'-a_{i-1},\qquad a_{i+1}-a_i'\)
以及必要时:\(a_i'+a_{i-1},\quad a_i'+a_{i+1},\quad a_i'\bmod k\)
核心启发:操作前后的相邻关系,有没有变成 swap / 加减 / 翻转 / 不变?

CF2252D 为例:

操作: \(a_i'=a_{i-1}-a_i+a_{i+1}\)
尝试定义差分 \(d_i=a_{i+1}-a_i\),会发现:

\[a_i'-a_{i-1}=a_{i+1}-a_i \]

\[a_i'-a_{i-1}=a_{i+1}-a_i \]

于是可以发现 \(\boxed{(d_{i-1},d_i)\to(d_i,d_{i-1})}\)
原数组上的复杂修改可以转化为差分数组上的相邻交换。
原条件要求 \(a_{i-1},a_{i+1}\text{ 同奇偶}\),可以翻译为:\(a_{i+1}-a_{i-1}=d_{i-1}+d_i\),等价于 \(\iff d_{i-1},d_i\text{ 同奇偶}\)

原问题彻底转化为:差分数组中,同奇偶的相邻元素可以交换。

赛场 Checklist

碰到“操作题想不出关键观察”,按这个顺序扫:

  1. 一次操作只看局部,写成 (x,y,z)。
  2. 算修改前后\(y-x,\quad z-y\)
  3. 看是否出现:交换 / 取反 / 平移 / 不变量
  4. 如果端点固定,重点试:差分
    如果操作是线性的,重点试:和、差、前缀和、异或、模 (k)
  5. 操作条件也翻译到新表示,不要只转换操作本身。
  6. 如果新表示下操作变成“交换”,立刻想:哪些元素能互相到达?能否任意排列?连通块是什么?

单调序列交错和为 0 方案数

定义一个序列 \(a\) 交错和是:下标为奇时加上 \(a_i\), 否则减去,得到的和。
因为数组单调:

  • 如果子序列长度是偶数,交替和 \(S = (b_1 - b_2) + (b_3 - b_4) + \dots\),因为每个括号里 \(b_{odd} \le b_{even}\),所以 \(S \le 0\)
  • 如果子序列长度是奇数,交替和 $S = b_1 - (b_3 - b_2) + \dots $,因为 \(b_{i + 1} \ge b_i\),所以 \(S \ge 0\)

结论: 正数部分的交替和 (S) 要是 0,长度必须是偶数,且括号里的数必须相等(即 \(b_1=b_2, b_3=b_4, \dots\))。
所以如果原单调序列都是正整数,只要记录每种数出现次数,将每种数选偶数个的方案数相乘即可。

如果如 CF2246C,序列内有 \(-1\) 呢?

如果最终答案序列长度为偶数,一定选偶数个 \(-1\),从而抵消。
否则,一定选奇数个 \(-1\),且后面的小括号内有且仅有一对数恰好差为 \(1\)

对于偶数情况,我们直接把 \(-1\) 看作正常的数,乘上其选偶数个的方案数即可。
对于奇数情况,统计刚好差 \(1\) 的种类数即可。

  • 为什么只需要统计种类数,不是说同一个值不同位置是不同方案?
    因为对同一个数,选奇数次和偶数次答案一样,不影响代码中的 W,详见【附】。
点击查看代码
map<ll, int> cnt; 
for (auto &p : cnt) W = W * pow2[p.second - 1] % MOD;

if (m == 0) cout << W << "\n";
else {
    ll q = 0;
    for(auto it : cnt) if(cnt.count(it.first + 1)) q++;     
    ll ans = W % MOD * (q + 1) % MOD;
    cout << ans << endl;
}

关于组合数【附】:对一个数选偶数个/奇数个个数

假设这个数出现了 \(c\) 次:
无论这一种值要求选奇数个还是偶数个,它对方案数量的贡献都是同一个 2^(cnt-1)
由二项式定理可以证明。

维护动态集合中的「最少取若干个值满足need」

可以考虑:动态维护有序的 Pool
典型形式:

pool = {};

for (int i = n; i >= 1; i--) {
    把第 i 组元素加入 pool;
    询问当前 pool 的某个最优值;
}

如:CF2252C,要找当前 pool 中,最少选择多少个最大的元素,才能使总和 ≥ need?

暴力方法:加完每组后对整个 \(pool\) 排序,然后遍历枚举前 \(k\) 个和直到大于 \(need\).
较劣方法:排序完,二分选择个数 \(k\),依然会产生同样的大量重复排序问题
我们考虑一种插入就能保持有序的结构,而且可以直接查出所需和所在的位置。

优化:先正难则反,最多删除多少个最小元素,使删除元素的总和 ≤ S - need?
\(budget = S - need;\),问题转化为从小到大,最长能取多少个,使前缀和 ≤ budget?
这是一个标准的有序前缀查询。

离散化所有权值,维护两棵 Fenwick:

bitCnt // 每个值出现次数
bitSum // 每个值的总贡献

于是每次插入:

bitCnt.add(pos[x], 1);
bitSum.add(pos[x], x);
totalCnt++;
totalSum += x;
// 这就等价于:pool.insert(x); 只是没有真的保存一个排序后的 pool。

查询时:

budget = totalSum - need; // 可以理解为可删空间

// 找最大 pos,使 bitSum.query(pos) <= budget
[pos, used] = bitSum.find(budget);

removed = bitCnt.query(pos);

// pos+1 这一档可能还能删一部分,因为budget不一定用完了
x = value[pos + 1];
remain = budget - used;
have = cnt(pos + 1);

removed += min(have, remain / x);

cur = totalCnt - removed;

CF2208D 构造一棵树满足可达性矩阵

现有一棵树,将其 \(n - 1\) 条边全部赋予一个方向。
给你一个 \(n ^ 2\) 的邻接矩阵。
矩阵一共 \(n\) 行,每一行包含一个字符串 \(s_i\)\(s_i\) 长度为 \(n\),只包含 \(0\)\(1\)。若 \(s_i\) 的第 \(j\) 个字符为 \(1\),表示在所有边定向后,节点 \(i\) 能到达节点 \(j\)
问你是否之真的存在一棵有向树能够满足矩阵的可达性关系。

简单版本,保证 \(n ^ 3 < 500 ^ 3\)

考虑 bitset 优化的 Floyd 维护传递闭包,在一定剪枝的情况下,同样的方法可能可以通过困难版本,但是复杂度存疑。

考虑可以直接判否的几种情况:

  • 自己必须能够到达自己。
  • 不能出现环,即以左上右下对角线为对称轴,不包括对角线本身,对称位置不能同时为 \(1\).
  • 可达性应该具有传递性,如果存在儿子能到的点父亲不能到,那就不行。
  • 树必须恰好有 \(n - 1\) 条边。
  • 树必须联通,不能为森林,可以使用并查集判断(\(hack:n = 5, 1 -> 2, 1 -> 3, 3 -> 4, 2 -> 4\), 点 \(5\) 不连通)。
点击查看代码
// 代码取自洛谷题解第一篇
inline void solve () {
    cin >> n;
    vector <bitset <N>>bit(n + 1);
    for (int i = 1; i <= n; ++ i) {
        cin >> s[i];
        for (int j = 1; j <= n; ++ j) {
            if (s[i][j - 1] == '1') bit[i].set (j);
        }
    }
    for (int i = 1; i <= n; ++ i)
        if (!bit[i][i]) { cout << "No" << endl; return; }//自己必须能到达自己
    for (int i = 1; i <= n; ++ i)
        for (int j = i + 1; j <= n; ++ j)
            if (bit[i][j] && bit[j][i]) { cout << "No" << endl; return; }//不能出现环
            
    for (int i = 1; i <= n; ++ i)
        for (int j = 1; j <= n; ++ j)
            if (bit[i][j] && (bit[j] & ~bit[i]).any ()) { cout << "No" << endl; return; }//第三种情况
            
    for (int i = 1; i <= n; ++ i) sz[i] = bit[i].count();
    vector <int> ans (n);
    iota (ans.begin(), ans.end(), 1);
    sort (ans.begin(), ans.end(), [&](int x, int y) {
        return sz[x] > sz[y];
    });//一个点能够到达的节点越多说明它在原树中越浅
    vector <pair <int, int>> edges;
    for (int i = 1; i <= n; ++ i) {
        bitset<N> dir = bit[i];
        dir.reset (i);
        vector <int> nn;
        for (int j : ans) {
            if (j == i) continue;
            if (bit[i][j]) nn.emplace_back(j);
        }
        for (int j : nn) {
            if (dir[j]) {
                edges.emplace_back(i, j);
                dir &= ~bit[j];//去除满足 i->j->k 的所有 i->k 的边
                // 这里写法很巧妙,而且由于直接儿子肯定在前面,故正确性有所保障
            }
        }
    }
    if (edges.size() != n - 1) { cout << "nO" << endl; return; }//不连通
    for (int i = 1; i <= n; ++ i) fa[i] = i, rk[i] = 1;
    for (auto [u, v] : edges) join (u, v);
    int rt = find(1);
    for (int i = 2; i <= n; ++ i)
        if (find(i) != rt) { cout << "nO" << endl; return; }//不连通
    cout << "yEs" << endl;
    for (auto [u, v] : edges) cout << u << " " << v << endl;
}

困难版本

此版本唯一区别是 \(n \le 8000\).

思路暂时没空写,先贴一个代码:

点击查看代码
// 摘自@block_in_mc
void solve() {
	int n; cin >> n;
	vector<string> vis(n + 10);
	for (int i = 1; i <= n; i++)
		cin >> vis[i], vis[i] = " " + vis[i];
	vector<int> deg(n + 10); queue<int> q;
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n; j++)
			if (j != i && vis[j][i] == '1')
				deg[i]++;
		if (deg[i] == 0) q.push(i);
	}
	vector<vector<int>> g(n + 10);
	vector<pair<int, int>> res;
	while (!q.empty()) {
		int p = q.front(); q.pop();
		vector<int> mk(n + 10), cnt(n + 10, -1);
		for (int i = 1; i <= n; i++)
			if (i != p && vis[i][p] == '1')
				mk[i] = 1;
		function<int(int)> dfs = [&](int p) {
			if (cnt[p] != -1) return cnt[p];
			cnt[p] = mk[p];
			for (int ed : g[p]) cnt[p] += dfs(ed);
			return cnt[p];
		};
		for (int i = 1; i <= n; i++)
			if (mk[i] == 1 && dfs(i) == 1)
				g[i].push_back(p), res.push_back({i, p});
		for (int i = 1; i <= n; i++)
			if (i != p && vis[p][i] == '1')
				if (--deg[i] == 0)
					q.push(i);
		if ((int) res.size() > n - 1) return cout << "No\n", void();
	}
	if ((int) res.size() != n - 1) return cout << "No\n", void();
	for (int i = 1; i <= n; i++) {
		string mk = " ";
		for (int j = 1; j <= n; j++)
			mk.push_back('0');
		function<void(int)> dfs = [&](int p) {
			if (mk[p] == '1') return;
			mk[p] = '1';
			for (int ed : g[p]) dfs(ed);
		}; 
		dfs(i);
		if (mk != vis[i]) return cout << "No\n", void();
	}
	vector<int> fa(n + 10); iota(fa.begin(), fa.end(), 0);
	function<int(int)> find = [&](int p) { return p == fa[p] ? p : (fa[p] = find(fa[p])); };
	for (auto [x, y] : res) {
		x = find(x), y = find(y);
		if (x == y) return cout << "No\n", void();
		fa[x] = y;
	}
	cout << "Yes\n";
	for (auto [x, y] : res)
		cout << x << ' ' << y << '\n';
}

给你一棵树和一个遍历序列,问你是否存在一种以 \(1\) 为根的树的 \(DFS\) 结果和给定序列相等。

来源:2026sdu省赛排位赛2 F

非常简单,把每个点的儿子按照给定序列的顺序排序,然后跑一遍 \(DFS\) 就行了。

点击查看代码
cin >> n;
    for(int i = 1; i < n; i++){
        int x, y;
        cin >> x >> y;
        g[x].push_back(y);
        g[y].push_back(x);
    }

    for(int i = 1; i <= n; i++) {
        cin >> a[i];
        pos[a[i]] = i;
    }

    for(int i = 1; i <= n; i++)
    sort(g[i].begin(), g[i].end(), [](int &x, int & y){
        return pos[x] < pos[y];
    });

    queue<int> qu;
    vector<int> tmp;
    vis[1] = 1;
    qu.push(1);
    while(!qu.empty()){
        int u = qu.front();
        qu.pop();
        tmp.push_back(u);
        for(auto v : g[u]){
            if(!vis[v]) {
                vis[v] = 1;
                qu.push(v);
            }
        }
    }

    for(int i = 1; i <= n; i++)
        if(tmp[i - 1] != a[i]){
            cout << "No" << endl;
            return 0;
        }

        cout << "Yes\n";

交互题 CF2209C Find the Zero

这是一个交互题。
给定一个整数 \(n\)。存在一个长度为 \(2n\) 的隐藏数组 \(a\)。从 \(1\)\(n\) 的每个整数在 \(a\) 中都恰好出现一次,其余元素均为 \(0\)
你可以进行如下类型的查询:

  • 选择两个整数 \(i\)\(j\)\(1 \le i, j \le 2n\)\(i \ne j\)),裁判会回应 \(1\),如果 \(a_i = a_j\),否则回应 \(0\)

请在不超过 \(n+1\) 次查询内,找到任意一个满足 \(a_k=0\) 的整数 \(k\)\(1 \le k \le 2n\))。注意交互方是自适应的,这意味着隐藏数组 \(a\) 可能会根据你的查询动态变化,但不会违反之前的查询结果。

题意:给你一个长为 \(2n\) 的数组,其中含有 $ 1 ~ n $ 各一个以及 \(n\)\(0\)。给你 \(n + 1\) 次交互机会,每次可以选两个位置的数字问他们是否相等,你需要找到任意一个 \(0\) 的位置。

注意到任意时刻的查询,只要两个数字相等,那他们一定都是 \(0\)

此时从大到小推不太可行,我们以小见大,先考虑 \(n = 2\) 的情况,此时有 \(4\) 个元素,\(3\) 次查询机会,集合\(\{0,0,x,y\}\)
我们先查 \((1, 2)\),如果相等,报告答案,如果不等,

我们再查 \((2, 3)\),如果相等,报告答案,如果不等,

我们再查 \((1, 3)\),如果相等,报告答案,如果不等,只剩下三种情况:
\(\{0, x, y, 0\},\{x, 0, y, 0\},\{x, y, 0, 0\}\)
\(\{0, x, y, 0\},\{x, 0, y, 0\}\)
此时位置 \(4\) 一定是 0.

考虑 \(n > 2\) 的情况。
我们直接利用刚刚的分组思想,对所有 \(i > 4\) 的其他元素,两个两个查一下,有相等就报告,
于是剩下的前 \(4\) 个数的集合一定是 \(\{0, 0, 0, 0\}\)\(\{0, 0, 0, x\}\)\(\{0, 0, x, y\}\) 之一。
利用刚刚 \(n = 2\) 的思路,这三种情况都可以用一样的方法解决。

代码:
注意交互题刷新缓冲区

点击查看代码
int n;

int solve(){
    for(int i = 5; i <= 2 * n; i += 2){
        printf("? %d %d\n", i, i + 1);fflush(stdout);
        int x;
        scanf("%d", &x);
        if(x == 1) return i;
    }
    int x;

    printf("? 1 2\n");fflush(stdout);
    scanf("%d", &x);
    if(x == 1) return 1;

    printf("? 1 3\n");fflush(stdout);
    scanf("%d", &x);
    if(x == 1) return 1;

    printf("? 2 3\n");fflush(stdout);
    scanf("%d", &x);
    if(x == 1) return 2;
    return 4;
}

int main(){
    int t;
    cin >> t;
    while(t--){
        cin >> n;
        printf("! %d\n", solve());fflush(stdout);
    }
	return 0;
}

P6619 [省选联考 2020 A/B 卷] 冰火战士

一场比赛即将开始。
每位战士有两个属性:温度和能量,有两派战士:冰系战士的技能会对周围造成降温冰冻伤害,因而要求场地温度不低于他的自身温度才能参赛;火系战士的技能会对周围造成升温灼烧伤害,因而要求场地温度不高于他的自身温度才能参赛。
当场地温度确定时,双方能够参赛的战士分别排成一队。冰系战士按自身温度从低到高排序,火系战士按自身温度从高到低排序,温度相同时能量大的战士排在前面。首先,双方的第一位战士之间展开战斗,两位战士消耗相同的能量,能量少的战士将耗尽能量退出比赛,而能量有剩余的战士将继续和对方的下一位战士战斗(能量都耗尽则双方下一位战士之间展开战斗)。如此循环,直至某方战士队列为空,比赛结束。
你需要寻找最佳场地温度:使冰火双方消耗总能量最高的温度的最高值。
现在,比赛还处于报名阶段,目前还没有任何战士报名,接下来你将不断地收到报名信息和撤回信息。其中,报名信息包含报名战士的派系和两个属性,撤回信息包含要撤回的报名信息的序号。每当报名情况发生变化(即收到一条信息)时,你需要立即报出当前局面下的最佳场地温度,以及该场地温度下双方消耗的总能量之和是多少。若当前局面下无论何种温度都无法开展比赛(某一方没有战士能参赛),则只要输出 Peace

输入格式

第一行一个数 \(Q\),表示信息的数量。
接下来 \(Q\) 行,每行为 1 t x y \((t \in \{0, 1\}\)\(x\)\(y\) 都是正整数 \()\)2 k\(k\) 是正整数):
1 t x y 表示一条报名信息,\(t = 0\) 时报名战士是冰系,\(t = 1\) 时报名战士是火系,\(x\) 表示战士的自身温度,\(y\) 表示战士的能量。
2 k 表示一条撤回信息,撤回的是第 \(k\) 条信息。被撤回的信息一定是报名信息,已被撤回的信息不会再次被撤回。

  1. 不用map,离散化完直接用 lower_bound 找到位置,替换掉所有 \(qus\)\(x\)
    用时 \(3.20s -> 1.43s\)
  2. 从前往后跳的二进制倍增,只适用于前缀数组,所以火的后缀数组需要用前缀数组的值减去总和来得到。
  3. 每次更新完后都要从头开始二分,找到最大的k,使得冰的前缀和不超过火的后缀和
    还要考虑 \(k+1\) 的情况,可能会有更大的答案,并且 \(k+1\) 的情况需要再从头开始二分,找到最大的 \(k_2\),使得冰的前缀和不超过火的后缀和或者两者相等(因为可能会有多个温度对应同一个答案)
    因为对于同一个 \(ans\) 可能是一段区间,题目要找最大的温度。
    3.2 第二次倍增必须从头开始,因为从中间倍增树状数组是错的。
    特别值得注意的是,不能只使用判等那一个条件,要或上小于等于的条件,先找到区间再移到最大的地方。
  4. 从前向后跳的树状数组倍增只支持前缀树状数组,从后向前则只支持后缀树状数组。
    那怎么维护后缀树状数组呢,可以用总和减去 sum[i - 1] 得到 sum[i] 的后缀和。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxq = 2e6 + 10;
int Q;
vector<int> lisan;
struct Question{
    int op;
    int t;
    int x, y;
} qus[maxq];

struct BIT{
    ll val[maxq];
    void change(int x, int y){
        for (; x < maxq; x += x & -x) val[x] += y;
    }
    ll query(int x){
        ll res = 0;
        for (; x; x -= x & -x) res += val[x];
        return res;
    }
} ice, fire;


ll sumf, sumi;

void pre();
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    pre();
    
    for (int i = 1; i <= Q; i++){
        if (qus[i].op == 1) {
            if (qus[i].t) { // fire
                fire.change(qus[i].x + 1, qus[i].y);
                sumf += qus[i].y;
            }
            else ice.change(qus[i].x, qus[i].y);
        }
        else { // delete
            Question q = qus[i];
            if (qus[q.t].t) {
                fire.change(qus[q.t].x + 1, -qus[q.t].y);
                sumf -= qus[q.t].y;
            }
            else  ice.change(qus[q.t].x, -qus[q.t].y);
        }
        int k = 0; // temperature
        ll ans = 0;
        ll tmpi = 0, tmpf = sumf;
        for (int d = pow(2, 20); d; d >>= 1) {
            if (k + d >= lisan.size()) continue;
            if (tmpi + ice.val[k + d] <= tmpf - fire.val[k + d]){
                tmpi += ice.val[k + d];
                tmpf -= fire.val[k + d];
                k += d;
            }
        }
        ans = max(ans, min(tmpi, tmpf) * 2);
        ll tmpans = 0;
        if(k + 1 < lisan.size()) tmpans = (sumf - fire.query(k + 1)) * 2;

        if(tmpans >= ans) {
            ans = tmpans;
            k++;
            int k_2 = 0;
            tmpi = 0;
            tmpf = sumf;

            for (int d = pow(2, 20); d; d >>= 1){
                if (k_2 + d >= lisan.size())continue;
                if (tmpi + ice.val[k_2 + d] <= tmpf - fire.val[k_2 + d] || min(tmpi + ice.val[k_2 + d], tmpf - fire.val[k_2 + d]) * 2 == ans){
                    tmpi += ice.val[k_2 + d];
                    tmpf -= fire.val[k_2 + d];
                    k_2 += d;
                }
            }
            k_2 = max(k_2, k);
            if (ans == 0)cout << "Peace\n";
            else cout << lisan[k_2] << " " << ans << "\n";
            continue;
        }
        if (ans == 0)cout << "Peace\n";
        else cout << lisan[k] << " " << ans << "\n";
    }

    return 0;
}
void pre(){
    cin >> Q;
    for (int i = 1; i <= Q; i++) {
        cin >> qus[i].op;
        if (qus[i].op == 1) {
            cin >> qus[i].t;
            cin >> qus[i].x >> qus[i].y;
            lisan.push_back(qus[i].x);
        }
        else cin >> qus[i].t;
    }
    lisan.push_back(0);
    sort(lisan.begin(), lisan.end()); 
    lisan.erase(unique(lisan.begin(), lisan.end()), lisan.end());

    for(int i = 1; i <= Q; i++) if (qus[i].op == 1) 
        qus[i].x = lower_bound(lisan.begin(), lisan.end(), qus[i].x) - lisan.begin();
        
    
}

枚举约数写法

来源:Codeforces Round 1078 (Div. 2) C题

枚举约数应该这样写,之前脑子傻了:

点击查看代码
void solve(){
    vector<int> divs;
    // 枚举 n 的所有约数 d,检查是否存在一个长度为 d 的周期满足条件
    // !!特别值得注意的是,枚举 n 的约数只需要枚举到 sqrt(n),每找到一个约数 i 就可以得到两个约数 i 和 n / i
    // 于是枚举一个数的所有可能约数的时间复杂度 d(n) 是 O(sqrt(n)) 的,而不是 O(n) 的
    for(int i = 1; i * i <= n; i++){ 
        // !!一定要写成i * i <= n,而不是i <= sqrt(n),因为精度问题可能导致漏掉一个约数
        if(n % i == 0){
            divs.push_back(i);
            if(i * i != n) divs.push_back(n / i);
        }
    }
    sort(divs.begin(), divs.end());
    for(int d : divs) if(check(d)) return; 
}

P1637 三元上升子序列

原始做法是我自己想的,按照题解二的说法:“缀和的思路、“逆序对”的目标进一步让我们联想到树状数组求逆序对。按刘汝佳大神的说法,“动态更新并求解前缀和,正是树状数组的标准用法”。”有了这一句话想到正解就不远了,所以略过。

推广做法采用 DP + 树状数组优化,可以 O(nmlog(n)) ,可以拓展至 n 元上升子序列问题

先仿照 LIS,假设 f[i][j] 表示以 \(a_j\) 结尾的长度为 i 的上升子序列个数.
于是状态转移方程为 \(\large{f[i][j] = \sum_{k<j,a[k]<a[j]}f[i-1][k]}\).
复杂度 \(O(n^2m)\),会崩。

二层循环中我们找当前下标的 前面的数 的上升子序列个数是一个个找的,可以采用树状数组“前缀和”维护当前下标有多少可以接上的序列,达到 log 级别的查找。
这个做法的巧妙程度真的令我拍案叫绝。

int main(){
cin >> n;
	for(int i = 1; i <= n; i++) cin >> a[i],  s[i] = a[i];
	sort(s+1, s+n+1);
	m = unique(s+1, s+n+1) - s - 1;
	for(int i = 1; i <= n; i++) f[1][i] = 1, a[i] = val(a[i]);
	for(int i = 2; i <= 3; i++) {
		memset(c, 0, sizeof(c));
		for(int j = 1; j <= n; j++) {
			f[i][j] = ask(a[j]-1);
			add(a[j], f[i-1][j]);
		}
	}
	for(int i = 1; i <= n; i++) ans += f[3][i];
	cout << ans << endl;

return 0;
}

三倍经验:CF597C
UVA12983 The Battle of Chibi

leetcode 3381 长度可被 K 整除的子数组的最大元素和

image

  • 这里有一个非常巧妙的处理方法
    此题先根据前缀和下标按 mod k 分组,同一组里随便选两个点,它们之间的子数组长度一定是 k 的倍数。
    于是对于同组内,问题转化为 \(\max (pre[j] - pre[i]) \quad \text{其中 } j > i\)
    这是一个经典套路:
    在从左到右遍历 j 的时候,维护目前看到的最小的 pre[i],然后试一试 pre[j] - min_pre

也就是“最大差值 = 当前值 - 之前最小值”。

CF2200F Mooclear Reactor 2

Bessie 需要在她的“mooclear”反应堆中尽可能多地产生能量。她有 \(n\) 种不同的粒子。
每个粒子由两个整数 \(x\)\(y\) 定义。该粒子可以产生 \(x\) 单位的能量,但具有反应性 \(y\),意味着该粒子只能与至多 \(y\) 个其它粒子一起存在于反应堆中。形式化来说,如果选择这个粒子来产生能量,那么最多只能再选择 \(y\) 个其它粒子(不包括自身)参与产生能量。
Bessie 必须选择一些满足上述限制的粒子子集来产生能量。她所能产生的能量总量等于被选择粒子的能量之和。
有一个商店提供 \(m\) 个粒子。Bessie 可以从商店中恰好购买一个粒子。对于商店里的每一个粒子,请你求出如果她只买这个粒子,那么她最多能产生的能量是多少。Bessie 并不要求一定要用上购买的粒子。

点击查看代码
int t;
int n, m;
ll dp[maxn];
// dp定义为,从旧粒子中选择k个,且这些粒子都满足 y >= k的最大能量和
struct particle{
    int x, y;
    friend bool operator < (particle a, particle b){
        return a.x > b.x; // small
    }
}p[maxn];
priority_queue<particle> qu;

void solve_one(){
    priority_queue<particle>().swap(qu);
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        cin >> p[i].x >> p[i].y;

    for(int i = 0; i <= n; i++) dp[i] = 0;
    sort(p + 1, p + 1 + n, [&](const particle a, const particle b){
        return a.y == b.y ? a.x > b.x : a.y > b.y;
    });

    int l = 1;
    ll now = 0, base = 0; // base 定义为,从旧粒子里边选 s 个且这些粒子满足 y >= s - 1
    // base和dp的作用是很有区别的
    // 因为dp为了接纳商店粒子,当限制为k的时候,我们要求qu.size() <= k,从而给商店粒子腾好位置
    // 而base要求qu.size() <= k + 1;
    for(int k = n; k >= 0; k--){
        while(l <= n && p[l].y >= k){
            now += p[l].x;
            qu.push(p[l++]);
        }
        while(qu.size() > k + 1){
            now -= qu.top().x;
            qu.pop();
        }
        base = max(base, now);
        if(qu.size() == k + 1){
            now -= qu.top().x;
            qu.pop();
        }
        dp[k] = now;
    }
    // 做前缀和是因为,在买入新的商店粒子的时候,我们可以选[0, y]个旧粒子
    for(int k = 1; k <= n; k++) dp[k] = max(dp[k], dp[k - 1]);

    for(int j = 1; j <= m; j++){
        ll x, y;
        cin >> x >> y;
        if(y == 0) cout << max(x, base) << " ";
        else cout << max({dp[y] + x, base}) << " ";
    }
    cout << "\n";
}
int main(){
    cin >> t;
    while(t--) solve_one();
	return 0;
}

cfEdu 185 C

这道题很有启发意义
首先xy可以理解为中间状态,我们要将qr形成最佳配对以得到最大配对个数
由于题意自然得到 x = y * q + r,把这个式子带到 \(x \le k\),得到\(qy + r \le k\),要满足这个方程给qr留出最大余量,于是y取最小值r + 1
于是便得到了一个q,r,k的相关方程,\(q(r + 1) + r \le k\)
对于一对q,r满足这个方程就意味着这是一组可行解
于是问题转化为最大二分图匹配
用qk表示r,得到 \(r = \frac{k - q}{q + 1}\),那么对于一个给定q,如果r满足这个式子,这一对qr就是可行的配对

点击查看代码
    sort (q + 1, q + 1 + n);
    sort (r + 1, r + 1 + n);
    int flagr = n;
    int ans = 0;
    while(flagr > 0 && r[flagr] >= k) flagr--;
    for(int i = 1, j = 1; i <= n && q[i] <= k && flagr; i++){
        ll rmax = (k - q[i]) / (q[i] + 1);
        while(flagr > 0 && rmax < r[flagr]) flagr--;
        if(flagr > 0 && rmax >= r[flagr]){
            flagr--;
            ans++;
        }
    }
    cout << ans << endl;

posted @ 2024-12-04 17:39  [丘李]Chilllee  阅读(39)  评论(0)    收藏  举报