【题解】CF2029C New Rating

思路(二分 + 数据结构优化DP)

大致题意为:一个值 \(x\) 初始为 \(0\),然后有一个数组 \(a\),遍历一次数组。
如果 \(a_i > x\),则 \(x + 1\)
如果 \(a_i < x\),则 \(x - 1\)
如果 \(a_i = x\),则 \(x\) 不变。
必须且只能跨越一段连续区间,求 \(x\) 的最大可能值。

后面的由前面的计算得到,并且前面的不受后面的影响,并且必须跳跃一段,很容易想到 DP。

首先是 DP 状态
最优解是开三个状态来 DP,这个官方题解讲得很清楚,因此这里提供一个我赛时写的两个状态的思路。
根据题目描述,可以连续也可以跳跃。
因此我选择给每个点 \(i\) 设两个状态:\(i\) 由前一个转移得到和由更前面的跳跃得到。
以下用 \(dp_{i, 0}\) 表示第 \(i\) 个点是由前面跳跃得到的,用 \(dp_{i, 1}\) 表示第 \(i\) 个点是由前一个点转移得到。

首先我们可以预处理一下一次也不跳的时候,每一个点的答案是多少,把它存入 \(dp_{i, 1}\) 中。这个很简单,按照题意从前往后模拟一遍即可。

然后我们考虑要跳跃的,也就是 \(dp_{i, 0}\) 的值。
因为题目要我们只能跳一次,所以,跳跃后的值只能通过没有跳过的值来转移,也就是 \(dp_{j, 1}\) 的值。

那么问题来了,只要和 \(i\) 有间隔的前面的点都可以作为转移的前置状态,选哪一个呢?
题目要我们求的是最后结果的最大值,不难发现,贪心地想,最后结果要最大,每一个局部的结果也要尽可能大。
因为这样的话,在 \(i\) 之后,经历相同的增增减减,在最后的答案一定会更大。
于是我们只有在这个点是增的时候才会更优,也就是在前面选择一个 \(dp_{i, 0}\) 的值比 \(a_i\) 小的点来转移。
简单证明:如果 \(dp_{j, 1} \geq a_i\),因为是从0增上来的,那么一定可以在 \(j\) 之前找到一个 \(k\),满足 \(dp_{k, 1} < a_i\),证毕。)
又因为结果要尽可能的大,所以要在更小的点中选一个最大的。
那么转移方程就出来了:\(dp_{i, 0} = \max(dp_{j, 1} + 1)\)\(j\) 满足 \(j < i - 1\) 并且 \(dp_{j, 1} < a_i\)

不难发现,如果暴搜的话,这样的复杂度是 \(O(n ^ 2)\) 的。
那再细品一下这句话:“比 \(a_i\) 更小的点中挑一个最大的。”那不就可以二分了吗。
我们可以把 \(j \in [1, i - 2]\)\(dp_{j, 1}\) 的值以及 \(0\) 压入一个便于二分数据结构中,比如 mapset
我赛时使用的树状数组(赛后发现 set 或者 map 就可以了而且复杂度更优)。
每次转移时,二分找到小于 \(a_i\) 中最大的一个值,用来转移得到 \(dp_{i, 0}\)
这个过程可以在循环的过程中同时进行,每次处理完一个 \(i\)\(dp_{i, 0}\) 后将 \(dp_{i - 1, 0}\) 压入数据结构即可。

到这里,我们就处理出了每一个点不跳跃到达和只跳跃一次到达能够得到的最大值。
要得到最后答案,我们还需要从前往后来一轮 DP。

这里我们再开一个 DP 数组 \(ans\)
\(ans_{i, 1}\) 表示到 \(i\) 为止没有跳跃过得到的最大值。
\(ans_{i, 0}\) 表示到 \(i\) 为止跳跃过了到的最大值。

那么转移也很明显。
因为 \(dp_{i, 1}\) 预处理的就是到 \(i\) 为止没有跳跃过,所以 \(ans_{i, 1} = dp_{i, 1}\)
\(ans_{i, 0}\) 因为表示的是到 \(i\) 为止跳跃过,所以我们只需要把在跳跃到 \(i\) 前面的位置和刚好跳跃得到 \(i\) 这个位置的结果取个最大值。
也就是假设跳跃到前面再不跳跃到 \(i\) 的答案设为 \(now\)
如果 \(ans_{i - 1, 0} < a_i\)\(now = ans_{i - 1, 0} + 1\)
如果 \(ans_{i - 1, 0} > a_i\)\(now = ans_{i - 1, 0} - 1\)
如果 \(ans_{i - 1, 0} = a_i\)\(now = ans_{i - 1, 0}\)
所以 \(ans_{i, 0} = \max(now, dp_{i, 0})\)
这里要特别注意一下,如果 \(i = 1\)\(ans_{1, 0}\) 应设置为一个极小值,因为 \(1\) 这个点是不能通过跳跃得到的,这个状态不存在。

最后取答案的时候,其实就是 \(i \in [1, n - 1]\) 时,和 \(ans_{i, 1}\) 取最小,表示前面都不跳,后面全跳。\(i = n\) 时,和 \(ans_{n, 0}\) 取最小,表示在前面已经跳跃过了并且到达了 \(n\) 这个点。

时间复杂度 \(O(n \times \log{n})\)
写题解的时候又想了一下,其实数据结构也可以不用,只需要记录到 \(i - 1\) 这个点时 \(dp_{j, 1}\) 的最大值就行了,因为是从 \(0\) 增长上来的,那么从 \(0\)\(\max(dp_{j, 1})\) 之间的数一定都出现过,这样的话 \(O(n)\) 就可以了。

AC CODE

#include <bits/stdc++.h>
#define int long long
#define inf 2e18
#define ull unsigned long long
#define ls o << 1
#define rs o << 1 | 1

using namespace std;

const int N = 3e5 + 9;
int a[N];
int dp[N][2];
int ans[N][2];
int t[N];
int n;

int lowbit(int x){return x & -x;}

void add(int v, int x)
{
	for(int i = v;i <= n;i += lowbit(i))t[i] += x;
}

int query(int v)
{
	int res = 0;
	for(int i = v;i;i -= lowbit(i))res += t[i];
	return res;
}

void init()
{
	for(int i = 1;i <= n;i ++)t[i] = 0;
	for(int i = 1;i <= n;i ++)
	{
		dp[i][0] = dp[i][1] = 0;
		ans[i][0] = ans[i][1] = 0;
	}
}

void solve()
{
	cin >> n;
	
	init();
	
	for(int i = 1;i <= n;i ++)cin >> a[i];
	
	int now = 0;
	for(int i = 1;i <= n;i ++)//预处理不跳跃的情况
	{
		if(a[i] > now)dp[i][1] = ++ now;
		else if(a[i] == now)dp[i][1] = now;
		else if(a[i] < now)dp[i][1] = -- now;
	}
	
	add(0 + 1, 1);
	for(int i = 2;i <= n;i ++)//得到跳到i这个位置的dp值
	{
		int pre = query(a[i]);
		int l = 0, r = a[i] + 1;
		while(l + 1 != r)//二分找最优(用set或map更优)
		{
			int mid = (l + r) >> 1;
			if(query(mid) < pre)l = mid;
			else r = mid;
		}
		dp[i][0] = r;
		add(dp[i - 1][1] + 1, 1);//将i - 1的没跳跃的dp值压入数据结构
	}
	
	for(int i = 1;i <= n;i ++)//处理到每个点为止的最优答案
	{
		ans[i][1] = dp[i][1];
		
		if(i == 1)ans[i][0] = -inf;
		else
		{
			int now = ans[i - 1][0];
			if(now > a[i])now --;
			else if(now < a[i])now ++;
			
			ans[i][0] = max(now, dp[i][0]);
		}
	}
	
	int res = 0;
	for(int i = 1;i < n;i ++)res = max(res, ans[i][1]);//跳最后一段
	res = max(res, ans[n][0]);//跳前面的
	
	cout << res << '\n';
}

signed main()
{
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);

    int t = 1;cin >> t;
    while(t --)solve();

    return 0;
}

posted @ 2024-12-10 18:28  天天超方的  阅读(60)  评论(0)    收藏  举报