关于 DP 的非常规优化

感觉这个东西就是玄学啊,考场上真的有人能想得出来嘛。(还是我太菜了qwq)

思想现在见到的有这几种:

  • \(i\) 推到 \(i + 1\) 时状态改变的数量不会太多,直接继承可以优化。

  • 可能对答案有贡献的状态不会太多。即通过一些性质来消除掉冗余状态以保证时间复杂度。

ABC176F Brave CHAIN

容易有简单 \(O(n^3)\) \(\mathrm {DP}\):设 \(f_{i, a, b}\) 是前 \(3\times i + 2\) 个保留 \((a, b)\) 时的最大答案。有一下三类转移:

  • 不换卡:\(f_{i, a, b} = f_{i - 1, a, b} + [c = d = e]\)

  • 换一张:\(f_{i, a, c} = f_{i - 1, a, b} + [b = d = e]\)

  • 换两张:\(f_{i, c, d} = f_{i - 1, a, b} + [a = b = e]\)

当然有一些类似的不予考虑。我们发现其实没有多少状态被影响到了,若被影响到的多,原因也是 \(c = d = e\),于是猜测影响到的状态不多。接下来考虑什么样的状态 \(f_{i, a, b}\) 可能会被这三类转移分别更新。

对于第一种,显然对于任意 \(f_{i, a, b}\) 有没有被更新取决于 \([c = d = e]\),而这三者又是固定的,于是维护一个全局加标记即可。

对于第二种,被影响到的状态只有 \(c\) 这一列,若 \([b = d = e]\) 成立,此时转移过来的状态是固定的。若不满足,其实就是对于固定的 \(a\),让 \(f_{i, a, c}\) 更新为这一行的最大值,维护一下即可。

对于第三种,若满足 \([a = b = e]\),两边都是确定的。直接转移即可。若不满足,显然等价于更新为全局最大值,也是维护一下即可。

qwq
#include<bits/stdc++.h>
using namespace std;

const int N = 2000 + 10;

int n, f[N][N], arr[N * 3];
void MAX(int& x, int y){if(x < y) x = y;}

struct opt{
	int k0, k1, val;
};
void upd(int a, int b, int w){
	MAX(f[a][b], w); MAX(f[a][0], w); MAX(f[0][0], w);
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n; memset(f, -0x3f, sizeof f); int allsum = 0;
	for(int i = 1; i <= 3 * n; i++) cin >> arr[i];
	upd(arr[1], arr[2], 0); upd(arr[2], arr[1], 0);
	for(int i = 1; i < n; i++){
		queue<opt> Q; int c = arr[3 * i], d = arr[3 * i + 1], e = arr[3 * i + 2], ad = 0;
		// f[i][a][b]
		if(c == d && d == e) ad++, allsum++;
		// f[i][a][c], f[i][a][d], f[i][a][e]
		for(int a = 1; a <= n; a++){
			if(d == e) Q.push((opt){a, c, f[a][d] + 1});
			Q.push((opt){a, c, f[a][0]});
			if(c == e) Q.push((opt){a, d, f[a][c] + 1});
			Q.push((opt){a, d, f[a][0]});
			if(d == c) Q.push((opt){a, e, f[a][d] + 1});
			Q.push((opt){a, e, f[a][0]});
		}
		// f[i][c][d], f[i][c][e], f[i][d][e]
		Q.push((opt){c, d, f[e][e] + 1}); Q.push((opt){c, d, f[0][0]});
		Q.push((opt){c, e, f[d][d] + 1}); Q.push((opt){c, e, f[0][0]});
		Q.push((opt){d, e, f[c][c] + 1}); Q.push((opt){d, e, f[0][0]});
		// update
		while(!Q.empty()){
			opt ths = Q.front(); Q.pop(); 
			int a = ths.k0, b = ths.k1, w = ths.val - ad;
			upd(a, b, w); upd(b, a, w);
		}
	}
	int ans = 0;
	for(int a = 1; a <= n; a++){
		for(int b = 1; b <= n; b++){
			MAX(ans, f[a][b] + (a == b && b == arr[n * 3]));
		}
	}
	cout << ans + allsum;

	return 0;
}
/*
f[i][a][b] = f[i - 1][a][b] + [c = d = e]
f[i][a][c] = f[i - 1][a][b] + [b = d = e]
f[i][c][d] = f[i - 1][a][b] + [a = b = e]
*/

AGC007E Shik and Travel

非常厉害的一道题!

首先容易发现答案具有单调性,于是先通过二分答案 \(V\) 转化成为判定问题。

接下来注意到一条边只能走两次,其实就是限制了当进入一棵子树,必须先把里面的所有点经过之后才能离开这棵子树。也就是说,当处理到一颗子树时,需要把它的一颗子树处理完,然后从一个叶子到另一颗子树的叶子,再处理完这个子树,然后离开。

不难发现我们只需要知道两颗子树从 \(u\) 到第一个叶子的距离以及最后一个叶子到 \(u\) 的距离。于是容易设计出状态 \(f_{u, a, b}\) 表示是否有一种处理方案满足费用 \(\le V\),且 \(u\) 到第一个叶子的距离为 \(a\),最后一个叶子到 \(u\) 的距离为 \(b\)。状态转移就是

\[f_{u, a, b} = f_{ls, a - da, i - da} \& f_{rs, j - db, b - db} \& [i + j \le V] \]

但是想要再优化就没有那么简单了。首先需要注意到一个显然的但是容易被忽略的性质:若 \(x_1 \le x_2, y_1 \le y_2\)\(f_{x_2, y_2}\)\(f_{x_1, y_1}\) 严格偏序,即没有用。于是我们可以将这些状态删掉,只记录有用的状态中值为 \(1\) 的,以减小时间复杂度。此时对于一个 \(u\),其有用的 \(f\) 值显然是满足 \(a\) 单调递增,\(b\) 单调递减。于是可以双指针合并一下两颗子树中的信息。

但是这样的时间复杂度?我们设节点 \(u\) 中储存的有用的状态数为 \(siz_u\)。一次转移产生的状态数上限是 \(2 \times \min(siz_{ls}, siz_{rs})\),不难发现这其实等价于一个启发式合并,于是时间复杂度为 \(O(n \log n)\)

code:

qwq
#include<bits/stdc++.h>
#define ll long long
#define pir pair<ll, ll>
using namespace std;

const int N = 2e5 + 10;

int n, son[N][3];
ll dis[N][2];
vector<pir> f[N], g[N], vec[N];
ll lim;

void clrvec(vector<pir> &v){vector<pir> __qwq; swap(__qwq, v);}

void Merge_array(int u){
    int j = 0; vector<pir> tmp;
    for(int i = 0; i < f[u].size(); i++){
           while(j < g[u].size() && (g[u][j].first < f[u][i].first || (g[u][j].first == f[u][i].first && g[u][j].second < f[u][i].second))) tmp.push_back(g[u][j++]);
           tmp.push_back(f[u][i]);
    }
    while(j < g[u].size()) tmp.push_back(g[u][j++]);
    if(tmp.empty()) return;
    int ttt = 0; vec[u].push_back(tmp[0]);
    for(int i = 1; i < tmp.size(); i++){
            if(tmp[i].second < vec[u][ttt].second) ttt++, vec[u].push_back(tmp[i]); 
    }
}

void dfs(int u){
    if(!son[u][2]){ vec[u].push_back(make_pair(0ll, 0ll)); return;}
    for(int i = 0; i < 2; i++) dfs(son[u][i]);
    int ls = son[u][0], rs = son[u][1], j = 0, sum = dis[u][0] + dis[u][1];
    for(int i = 0; i < vec[ls].size(); i++){
           while(j < vec[rs].size() && vec[ls][i].second + vec[rs][j].first + sum <= lim) j++;
           if(j) f[u].push_back(make_pair(vec[ls][i].first + dis[u][0], vec[rs][j - 1].second + dis[u][1]));
    }
    swap(ls, rs); j = 0;
    for(int i = 0; i < vec[ls].size(); i++){
           while(j < vec[rs].size() && vec[ls][i].second + vec[rs][j].first + sum <= lim) j++;
           if(j) g[u].push_back(make_pair(vec[ls][i].first + dis[u][1], vec[rs][j - 1].second + dis[u][0]));
    }
    Merge_array(u); clrvec(f[u]); clrvec(g[u]);
    //cout << u << " " << vec[u].size() << " qwq" << "\n";
    //for(int i = 0; i < vec[u].size(); i++) cout << vec[u][i].first << " " << vec[u][i].second << "\n";
}


bool check(ll V){
       //cout << "\n" << V << "\n" << ":::" << "\n";
       for(int i = 1; i <= n; i++) clrvec(f[i]), clrvec(g[i]), clrvec(vec[i]);
       lim = V; dfs(1);
       return vec[1].size();
}

signed main(){
       ios::sync_with_stdio(0);
       cin.tie(0); cout.tie(0);
       cin >> n;
       for(int i = 2; i <= n; i++){
            int x, y; cin >> x >> y;
            son[x][son[x][2]] = i; dis[x][son[x][2]] = y; ++son[x][2];
       }
       ll l = 0, r = 3e10, ans = 3e10;
       while(l <= r){
            ll mid = (l + r >> 1);
            if(check(mid)) r = mid - 1, ans = mid;
            else l = mid + 1;
       }
       cout << ans;

       return 0;
}

也是学会使用 \(\rm vim\) 了qwq

posted @ 2024-11-09 22:17  Little_corn  阅读(7)  评论(0编辑  收藏  举报