Luogu P12568 [UOI 2023] An Array and Range Additions 题解 [ 蓝 ] [ 线性 DP ] [ 倍增 ] [ adhoc ]
An Array and Range Additions:简单思维题,但我没场切,抱抱我真堂。
注意到 \(x\) 可以选任意数,所以对一个区间加 \(x\) 的操作可以等效于将这个区间里的数与区间外的数区分开来,因为 \(x\) 可以直接拉到一个很大的数,下一次操作可以拉到更大的数,以此类推。
因为把这个区间与其他区间分隔开了,所以相当于把这个序列切成了三段。而对于后续的区间加也是同理,会在两个端点所在的块内把序列分割。这是本题最关键的性质。由此可以得到,如果进行了 \(x\) 次分割,那么至少要进行 \(\left \lceil \dfrac{x}{2} \right \rceil\) 次区间加操作,因为每次区间加会导致两次分割。
接下来考虑利用这个性质,直接对最后形成的序列进行分析,在把这个序列分割成多个段后,如果某个段内仍然存在相同的数字,显然是无法满足要求的。注意第一段和最后一段是特殊的,因为我们分割序列的时候区间加的都是中间段,所以第一段和最后一段是不需要进行区间加的,但是需要满足第一段和最后一段合起来每个数字均出现一次。
于是问题被转化为:对一个序列进行分割,使得每一段内均无重复数字出现,且第一段和最后一段需要特殊考虑。求最小分割次数。
首先可以观察到一个单调性:如果一段的左端点已确定,那么当右端点最右的时候一定是最优的。因此,在左端点确定的时候,我们能够确定的段是唯一的,因为这个段就是左端点和最大右端点所组成的段。为了求出最大右端点,可以根据单调性用一个双指针和 unordered_map 进行维护。
剩下的 DP 是显然的,对于中间段,可以直接找到最大右端点进行转移;而对于首尾段,DP 的过程中显然不可以多记录一维,而前后要保证无重复元素,可以把他们接成同一个段。因此考虑“断环为链”的 trick,把数组复制一倍,如果此时的右端点跳出了 \(n\) 就直接停止,根据左端点判断是否计入答案即可。
这一部分可以用倍增或者直接递推实现。如果直接递推即可做到时间复杂度 \(O(n)\)。
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi=pair<int,int>;
const int N = 400005, inf = 0x3f3f3f3f;
int n, a[N], to[N], dp[N], ans = inf;
unordered_map<int, int> tot;
int main()
{
//freopen("sample.in","r",stdin);
//freopen("sample.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
a[i + n] = a[i];
to[i + n] = i + n - 1;
}
int p = 1;
for(int i = 1; i <= n; i++)
{
while(tot[a[p]] == 0)
{
tot[a[p]]++;
p++;
}
to[i] = p;
tot[a[i]]--;
}
if(to[1] > n)
{
cout << 0;
return 0;
}
for(int i = n; i >= 1; i--)
{
dp[i] = dp[to[i]] + 1;
to[i] = to[to[i]];
}
for(int i = 1; i <= n; i++)
if(to[i] >= i + n - 1)
ans = min(ans, dp[i]);
cout << (ans + 1) / 2;
return 0;
}

浙公网安备 33010602011771号