ABC370 DEF 题解

ABC370 DEF 题解

赛时过了 ABCD,补题的时候发现 EF 其实也是简单题,为什么就做不出来呢?E 这样简单的 dp 都做不出来,dp 必须得多练啊!

D - Cross Explosion

题目链接

对于每一行、列,我们要用一个数据结构来维护未被删除的点,并且要快速找到某一行/列中是否存在某个数,以及查询某个数的前驱/后继。自然想到使用 set

或许只看第一个要求:维护未被删除的点,会想到用链表。但链表的随机访问是线性复杂度的,不能快速查询某个数的位置。

然后直接按题意模拟即可。这一步主要考察对 set 的使用,刚好我这几天一直在加强学习 STL,感觉已经逐渐熟能生巧了,代码还是比较好看的(

代码实现上的一些细节:

  1. 可以用 set.lower_bound(x) 来查询 x 是否存在。设返回的迭代器为 it,如果存在,那么 *it == x。如果不存在,那 it 就指向 x 的后继,prev(it) 指向 x 的前驱。(这里说前驱后继指的是假设 x 存在,x 的前驱后继。)

    set.find(x) 也可以查询 x 是否存在,但是如果它不存在,用 find() 不好查找前驱后继。

    以及,不要用 algorithm 库中的 lower_bound(),要用 set 自带的 lower_bound(),否则无法保证单次查找的时间复杂度是对数。

  2. 每次删除一个点,要同时在维护行和列的 set 中删除。(一开始没注意到这个调了好久)

int n, m, q, cnt;
vector<set<int>> row, col;

void del(int x, int y)
{
	cnt--;
	auto it_row = row[x].find(y);
	assert(it_row != row[x].end());
	row[x].erase(it_row);
	
	auto it_col = col[y].find(x);
	assert(it_col != col[y].end());
	col[y].erase(it_col);
}

int main()
{
	cin >> n >> m >> q;
	row.resize(n + 1), col.resize(m + 1);
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= m; j++) row[i].insert(j);
	for(int i = 1; i <= m; i++)
		for(int j = 1; j <= n; j++) col[i].insert(j);
	
	cnt = n * m;
	for(int i = 1, x, y; i <= q; i++)
	{
		cin >> x >> y;
		auto it_row = row[x].lower_bound(y);
		if(*it_row == y) del(x, y);
		else
		{
			if(it_row != row[x].begin()) del(x, *(prev(it_row)));
			if(it_row!= row[x].end()) del(x, *it_row);
			
			auto it_col = col[y].lower_bound(x);
			if(it_col != col[y].begin()) del(*(prev(it_col)), y);
			if(it_col != col[y].end()) del(*it_col, y);
		}
	}
	
	cout << cnt << endl;
	
	return 0;
}

提交记录

E - Avoid K Partition

题目链接

首先要想到用 dp,但是我连这一步都没想到,哈哈哈。

dp 还是得多做啊。

f(i) 表示 A1Ai 的合法划分方案数。易得转移方程:

f(i)=j=0i1[sum(j+1i)K]×f(j)

这里注意一下初始化的问题:我们应初始化 f(0)=1。为什么呢?可以从转移的角度考虑:j=0 时,我们考虑 1i 这一段的和。如果和不为 K,我们就加上 f(0),这就相当于已知把 1i 当成一整段时,问 1i 划分的方案数——显然就是 1

用上面这个转移方程转移是 O(N2) 的。考虑优化。

f(i) 时,我们相当于是在 0j<i 中寻找那些使得 sum(j+1i)Kj。用 sum 表示前缀和,就相当于寻找那些使得 sum(j)sum(i)Kj,然后求这些 f(j) 的和。

于是可以设 M(x) 表示当前(枚举到 i 时)所有使得 sum(j)=xf(j) 的和,那么转移方程可以改写成:

f(i)=(j=0i1f(j))M(sum(i)K)

同时再维护一下 M 即可:M(sum(i))M(sum(i))+f(i)。初始化 x,M(x)=0M 可以用 map 维护。(用 map 的话,实际上可以不用初始化,因为 map 保证第一次访问 M(x) 时它的值为 0。)总时间复杂度 O(NlogN)

f[0] = 1;
ll all = 1;
map<ll, ll> mp;
mp[0] = 1;
for(int i = 1; i <= n; i++)
{
    f[i] = ((all - mp[sum[i] - K]) % MOD + MOD) % MOD;
    all = (all + f[i]) % MOD;
    mp[sum[i]] = (mp[sum[i]] + f[i]) % MOD;
}

细节:作为 map 下标的数(这里是 sum[i] - K)是不能取模的,原因一是它不会爆 long long,二是不同的数取模的结果可能相同,可能导致冲突(我就因为这个爆了)。

提交记录

F - Cake Division

题目大意:有一个环状序列,要把它分成 K 段。设每段的权值为该段中所有数的和,你需要最大化最小的权值。可能有多种划分方案使得最小权值最大,你还需要求出在这些划分方案中,有哪些地方从未被划开过。

因为一些细节调了很久(恼)

先考虑如何求出最大的最小权值。

题目是个环,有点麻烦。先考虑序列是个链时怎么做。这时显然有一个简单的二分:设 x 表示最小值,然后可以 O(N) 地判定在每一段的和都大于 x 的情况下,能否把序列分为至少 K 段。

到了环上,一个经典的 trick 是断环成链:把原序列复制接在它自己的后面。然后,我们还是想到二分,但不能用上面的方法判定了。

f(i) 表示在 i+12×N+1 范围内,第一个使得 sum(if(i)1)x 的数。换句话说,如果 i 是某一段的开头,这一段至少要到 f(i)1 结束才能满足该段的权值不小于 xf(i) 就是这一段的下一个位置。(这里定义的 f(i) 往后挪了一位,主要是为了方便后续操作。当然不挪这一位也是可以的。)如果不存在这样的数,则 f(i)=+。运用双指针的技巧,f(i) 可以在 O(N) 的时间内求出。

f(i) 的妙处在于它是可以自身复合的:f(f(i)) 表示从 i 开始找两段以后的下一个位置,同理 fK(i) 就表示从 i 开始找 K 段以后的下一个位置。于是,只要存在 1iN 使得 fK(i)N+i,此时的 x 就是可行的。而 fK(i) 可以通过倍增在 O(NlogK) 的时间内求出。

这里要注意一下 + 的问题:实现时,+ 应该是一个比所有的 f(i) 都大的数,而 f(i)2×N+1,所以 + 可以设为 2×N+2。另外,我们得保证 f(12×N+2) 中所有的数都有值,所以初始化 f(2×N+1)=f(2×N+2)=2×N+2

接下来考虑第二个问题。

直接考虑并不容易。我们可以先求出:有多少个地方,断开之后能满足条件?这是容易的,沿用上文中的方法:只要 fK(i)N+i,那么 ii1 之间就可以断开,同时满足最小权值不小于 x。又因为我们总共有 N 个地方可以断开,所以用 N 减去这个数就是答案。

int n, K;
cin >> n >> K;
vector<int> a(n + 1), sum(2 * n + 1);
for(int i = 1; i <= n; i++)
{
    cin >> a[i];
    sum[i] = sum[i-1] + a[i];
}
for(int i = n + 1; i <= 2 * n; i++)
    sum[i] = sum[i-1] + a[i-n];

vector<vector<int>> f(2 * n + 3);
for(auto &vec: f) vec.resize(__lg(K) + 1);

auto check = [&](int x) -> int // 返回需要切开的切线的数量 
{
    for(int i = 1, j = 1; i <= 2 * n; i++)
    {
        while(j < 2 * n && sum[j] - sum[i - 1] < x) j++;
        if(sum[j] - sum[i-1] >= x) f[i][0] = j + 1;
        else f[i][0] = 2 * n + 2; // 相当于INF 
    }
    f[2 * n + 1][0] = 2 * n + 2;
    f[2 * n + 2][0] = 2 * n + 2;

    int t = __lg(K);
    for(int k = 1; k <= t; k++)
        for(int i = 1; i <= 2 * n + 2; i++)
            f[i][k] = f[f[i][k-1]][k-1];

    int cnt = 0; // 能作为起点的点数 
    for(int i = 1; i <= n; i++) // 枚举每个点,判断其是否能成为起点
    {
        int en = i;
        for(int j = 0; (1 << j) <= K; j++)
            if(K & (1 << j)) en = f[en][j];
        if(en <= n + i) cnt++;
    }
    return cnt;
};

i64 l = 0, r = sum[n], ans = -1;
int cnt = -1;
while(l <= r)
{
    i64 mid = (l + r) >> 1;
    int res = check(mid);
    if(res != 0) // 可行(至少有一个解) 
    {
        l = mid + 1;
        ans = mid, cnt = n - res;
    }
    else r = mid - 1;
}

cout << ans << ' ' << cnt << endl;

提交记录

posted @   DengStar  阅读(53)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示