• CF1420E Battle Lemmings 斜率优化


    题目来源:CodeforcesCodeforces Round #672 (Div. 2),CF1420,E 题,Battle Lemmings。

    题目大意

    题目链接

    有一排 (n) 个[林檎实],有些[林檎实]拿着盾牌,有些没有。我们称一对[林檎实]是被保护的,当且仅当它们都没有拿盾牌,且在他们之间有有人拿着盾牌。

    你可以进行若干次操作,每次操作是如下两种之一:

    • 选择一个拿着盾牌,且他左边的人没盾牌的[林檎实] (i) ((1< ileq n)),将第 (i) 个[林檎实]的盾牌给他左边的人。
    • 选择一个拿着盾牌,且他右边的人没盾牌的[林檎实] (j) ((1leq j<n)),将第 (j) 个[林檎实]的盾牌给他右边的人。

    现在,给你一个长度为 (n)(01) 序列,表示初始时每个[林檎实]是否拿着盾牌。

    [林檎实]的妹子随时会过来找他约会,所以他也不知道他还能进行几次操作。因此,对于所有 (kin[0,frac{n(n-1)}{2}]),请算出做不超过 (k) 次操作时,最多能有多少对被保护的[林檎实]。也就是说,你需要输出 (frac{n(n-1)}{2}) 个数。

    数据范围:(1leq nleq 80)

    本题题解

    设没拿盾牌的点数量为 (c_0),拿着盾牌的点数量为 (c_1)

    初步转化 1:考虑两个 (0) 不是被保护的,当且仅当它们之间没有 (1)。也就是说,被保护的对数 = 总对数 - 在同一段里的 (0) 的对数。具体来说,设有 (k) 段极长、连续的 (0),长度分别为 (l_1,l_2,dots,l_k),则被保护的对数 (=frac{c_0(c_0-1)}{2}-sum_{i=1}^{k}frac{l_i(l_i-1)}{2})。于是我们只要最小化 (sum_{i=1}^{k}frac{l_i(l_i-1)}{2}) 即可。

    初步转化 2:考虑已知一个目标局面(也就是一个 (01) 序列),那么从初始局面到这个目标局面的最少操作次数,显然就是两个局面里 第一个 (1)、第二个 (1)、...、第 (c_1)(1) 分别的坐标差之和。也就是说,初始局面的每个 (1) 和目标局面的每个 (1),按顺序依次对应,可以证明这样操作是最优的(调整法)。记初始局面里每个 (1) 的位置分别为 (p_1,p_2,dots,p_{c_1})

    有了上述两个结论,我们可以通过 DP 来确定目标序列,顺便求出未被保护的对数(sum_{i=1}^{k}frac{l_i(l_i-1)}{2}))和操作次数

    (dp[i][x][y][z]) 表示考虑了前 (i) 位,用了 (x) 次操作,前 (i) 位里总共有 (y)(1),从最后一个 (1)(i) 之间这最后一段 (0) 的长度为 (z),这个局面下未被保护的对数的最小值。转移时考虑第 (i+1) 位是填 (0) 还是填 (1) 即可。时间、空间复杂度都是 (O(n^5))(因为 (x) 这一维大小是 (frac{n(n-1)}{2}=O(n^2)) 的,不要忘了)。可以用滚动数组优化空间,不过这种做法时间、空间常数都较大。

    考虑简化状态定义。设 (dp[i][x][y]) 表示考虑了前 (i) 位,(i) 位上填 (1),用了 (x) 次操作,前 (i) 位里总共有 (y)(1),这个局面下未被保护的对数的最小值。转移时枚举下一个 (1) 在哪里。这样时间复杂度依然是 (O(n^5)),但空间复杂度优化到了 (O(n^4))。本做法的 AC 代码请见【参考代码1】。

    继续优化。考虑先枚举 (x,y),再枚举 (i)。此时能转移到 (dp[i][x][y]) 的,一定是 (x'=x-|p_y -i|)(y'=y-1)。也就是说 (x')(y') 都是确定的,我们只需要找到最优的 (j) ((j<i)),然后用 (dp[j][x'][y']) 更新 (dp[i][x][y]) 即可。(O(n^5)) 的做法相当于是枚举了 (j)。考虑如何不枚举,快速求出最优的 (j)

    具体来说,我们先观察一下转移式:

    [dp[i][x][y] = min_{j<i}{dp[j][x'][y'] + frac{(i - j - 1)(i - j - 2)}{2}} ]

    首先可以去掉“除以 (2)”,在求答案的时候一起除。另外,因为 (x,y,x',y') 都是常数,不妨将它们写在前面。于是我们重新定义状态:(f_{x,y}(i) = dp[i][x][y] imes2)。然后把上式中的 ((i - j - 1)cdot (i - j - 2)) 拆开,得到:

    [f_{x,y}(i) = min_{j<i}{f_{x',y'}(j)+i^2-3i+j^2+3j-2ij+2} ]

    其中 (i^2), (-3i), (2) 都是常数,可以提出来。关键的部分是 (-2ij),可以用斜率优化搞定它。具体来说,把每个 (j) 看成二维平面上一个坐标为 ((j,f_{x',y'}(j)+j^2+3j)) 的点,转移看成一条斜率为 (2i) 的直线,我们要最小化直线的截距(也就是 (f_{x,y}(i)) 加上一堆关于 (i) 的常数)。写成式子就是:

    [f_{x',y'}(j)+j^2+3j=2ij+f_{x,y}(i)-i^2+3i-2 ]

    对每个 ((x',y')) 分别维护一个下凸壳,然后在凸壳上二分出第一段斜率大于 (2i) 的位置即可。

    时间复杂度 (O(n^4log n))。空间复杂度 (O(n^4))。本做法的 AC 代码请见【参考代码2】。

    参考代码

    参考代码1:时间复杂度 (O(n^5)),空间复杂度 (O(n^4))

    // problem: CF1420E
    #include <bits/stdc++.h>
    using namespace std;
    
    #define pb push_back
    #define mk make_pair
    #define lob lower_bound
    #define upb upper_bound
    #define fi first
    #define se second
    #define SZ(x) ((int)(x).size())
    
    typedef unsigned int uint;
    typedef long long ll;
    typedef unsigned long long ull;
    typedef pair<int, int> pii;
    
    template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
    template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
    
    const int MAXN = 80;
    const int INF = 0x3f3f3f3f;
    int n, a[MAXN + 5], cnt[2], pos[MAXN + 5];
    int total, max_op;
    int dp[MAXN + 5][MAXN * (MAXN - 1) / 2 + 5][MAXN + 5];
    int calc(int len) { return len * (len - 1) / 2; }
    
    int main() {
    	cin >> n;
    	for(int i = 1; i <= n; ++i) {
    		cin >> a[i];
    		cnt[a[i]]++;
    		if(a[i] == 1) {
    			pos[cnt[1]] = i;
    		}
    	}
    	total = calc(cnt[0]);
    	max_op = calc(n);
    	if(!cnt[1]) {
    		for(int i = 0; i <= max_op; ++i) cout << 0 << " "; cout << endl;
    		return 0;
    	}
    	
    	memset(dp, 0x3f, sizeof(dp));
    	for(int i = 1; i < n; ++i) {
    		dp[i][abs(pos[1] - i)][1] = calc(i - 1);
    		for(int j = 0; j <= max_op; ++j) {
    			for(int k = 1; k <= i && k < cnt[1]; ++k) if(dp[i][j][k] != INF) {
    				int rest = cnt[1] - k;
    				for(int l = i + 1; l <= n - rest + 1; ++l) {
    					int newj = j + abs(pos[k + 1] - l);
    					if(newj <= max_op)
    						ckmin(dp[l][newj][k + 1], dp[i][j][k] + calc(l - i - 1));
    				}
    			}
    		}
    	}
    	int res = total;
    	for(int j = 0; j <= max_op; ++j) {
    		for(int i = cnt[1]; i <= n; ++i) if(dp[i][j][cnt[1]] != INF) {
    			ckmin(res, dp[i][j][cnt[1]] + calc(n - i));
    		}
    		cout << total - res << " ";
    	}
    	cout << endl;
    	return 0;
    }
    

    参考代码2:时间复杂度 (O(n^4log n)) 斜率优化做法。

    // problem: CF1420E
    #include <bits/stdc++.h>
    using namespace std;
    
    #define pb push_back
    #define mk make_pair
    #define lob lower_bound
    #define upb upper_bound
    #define fi first
    #define se second
    #define SZ(x) ((int)(x).size())
    
    typedef unsigned int uint;
    typedef long long ll;
    typedef unsigned long long ull;
    typedef pair<int, int> pii;
    
    template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
    template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
    
    const int MAXN = 80, MAXOP = MAXN * (MAXN - 1) / 2;
    const int INF = 0x3f3f3f3f;
    int n, a[MAXN + 5], cnt[2], pos[MAXN + 5];
    int total, max_op;
    int dp[MAXOP + 5][MAXN + 5][MAXN + 5];
    struct Queue {
    	int a[MAXN + 5];
    	int ql, qr;
    	int size() { return qr - ql + 1; }
    	bool empty() { return ql > qr; }
    	int front() { return a[ql]; }
    	int back() { return a[qr]; }
    	int back2() { return a[qr - 1]; }
    	void push_back(int e) { a[++qr] = e; }
    	void pop_back() { --qr; }
    	void init() { ql = 1; qr = 0; }
    }que[MAXOP + 5][MAXN + 5];
    
    int calc(int len) { return len * (len - 1) / 2; }
    int calc2(int len) { return len * (len - 1); }
    
    int main() {
    	cin >> n;
    	for(int i = 1; i <= n; ++i) {
    		cin >> a[i];
    		cnt[a[i]]++;
    		if(a[i] == 1) {
    			pos[cnt[1]] = i;
    		}
    	}
    	total = calc(cnt[0]);
    	max_op = calc(n);
    	if(!cnt[1]) {
    		for(int i = 0; i <= max_op; ++i) cout << 0 << " "; cout << endl;
    		return 0;
    	}
    	
    	memset(dp, 0x3f, sizeof(dp));
    	for(int j = 0; j <= max_op; ++j) {
    		for(int k = 1; k <= cnt[1]; ++k) {
    			que[j][k].init();
    		}
    	}
    	for(int i = 1; i <= n; ++i) {
    		#define Q que[abs(pos[1] - i)][1]
    		#define F dp[abs(pos[1] - i)][1]
    		#define Y(p) (F[(p)] + (p) * (p) + 3 * (p))
    		
    		F[i] = calc2(i - 1);
    		while(Q.size() >= 2 && (Y(Q.back()) - Y(Q.back2())) * (i - Q.back()) > (Y(i) - Y(Q.back())) * (Q.back() - Q.back2()))
    			Q.pop_back();
    		Q.push_back(i);
    		
    		#undef Q
    		#undef F
    		#undef Y
    	}
    	for(int j = 0; j <= max_op; ++j) {
    		for(int k = 2; k <= cnt[1]; ++k) {
    			for(int i = k; i <= n - (cnt[1] - k); ++i) {
    				if(j < abs(pos[k] - i)) continue;
    				int last_j = j - abs(pos[k] - i);
    				#define Q que[last_j][k - 1]
    				#define F dp[last_j][k - 1]
    				#define Y(p) (F[(p)] + (p) * (p) + 3 * (p))
    				if(Q.empty()) continue;
    				if(Q.front() >= i) continue;
    				int l = Q.ql;
    				int r = Q.qr;
    				while(l < r) {
    					int mid = (l + r + 1) >> 1;
    					if(Q.a[mid] < i) {
    						l = mid;
    					} else {
    						r = mid - 1;
    					}
    				}
    				l = Q.ql;
    				while(l < r) {
    					int mid = (l + r) >> 1;
    					int p1 = Q.a[mid], p2 = Q.a[mid + 1];
    					if(Y(p2) - Y(p1) > 2 * i * (p2 - p1)) {
    						r = mid;
    					} else {
    						l = mid + 1;
    					}
    				}
    				ckmin(dp[j][k][i], F[Q.a[l]] + calc2(i - Q.a[l] - 1));
    				#undef Q
    				#undef F
    				
    				#define Q que[j][k]
    				#define F dp[j][k]
    				while(Q.size() >= 2 && (Y(Q.back()) - Y(Q.back2())) * (i - Q.back()) > (Y(i) - Y(Q.back())) * (Q.back() - Q.back2()))
    					Q.pop_back();
    				Q.push_back(i);
    				
    				#undef Q
    				#undef F
    				#undef Y
    			}
    		}
    	}
    	int res = total;
    	for(int j = 0; j <= max_op; ++j) {
    		for(int i = cnt[1]; i <= n; ++i) if(dp[j][cnt[1]][i] != INF) {
    			ckmin(res, (dp[j][cnt[1]][i] + calc2(n - i)) / 2);
    		}
    		cout << total - res << " ";
    	}
    	cout << endl;
    	return 0;
    }
    
  • 相关阅读:
    API
    Object constructor
    function()
    For语句的衍生对象
    编程语言发展史
    将Paul替换成Ringo
    Document.write和 getElementById(ID)
    姓名,电话号码,邮箱的正则检测
    JavaScript-BOM与DOM
    管理心得
  • 原文地址:https://www.cnblogs.com/dysyn1314/p/13729196.html
Copyright © 2020-2023  润新知