【5 - 3】模考总结

死亡回放

【8:30 - 8:45】看 T1,很简单的样子。15 min秒了。
【8:46 - 9:17】看 T2,看起来很板呢。推了下规律,发现这个子序列的生成过程像一个栈,但并不知道如何用栈实现 qwq。其实是可以用单调栈实现的,上这节课的时候是录播课,没搞懂……于是蒟蒻换了种思路写,两个样例都过了。
【9:18 - 10:02】看 T3,应该是数学题。当时手推了一下性质,凭着自己的直觉乱搞了一下,推出了公式!!!在赛后老师讲了题,我才知道那个东西叫“整除分块”。
【10:03 - 11:15】看 T4,是个 dp,好难啊。写啊写啊写啊,写了一个记忆化 + dp。调试完有一个样例没过。于是加了一个特判。
【11:15 - 12:00】检查题目,结果把自己的 T2 解法 hack 了。。。也不想改了,觉得用该能过一点样例。开摆……

估分:

100 + [0, 70) + [60, 100] + 20

实际:

100 + 0 + 100 + 11

考试总结

犯了几个错

  • 怎么能早早摆烂呢?。

发现自己的解法被 hack 了,应该去重新修改啊!不会单调栈暴力写也能拿 30 pts。

  • 为什么那么相信自己的写法?。

如果实在不会(如 T4),完全没有把握可以写一些人类智慧骗分,不需要敲自己认为是正解但成功几率不大的代码。像 hhy 大佬,T4敲了个贪心 + 暴力的人类智慧,能拿 80!

考试题解

T1 存钱
够水的。容易发现将最小的两个加起来是最优的,且能得到一个零,且这个零可以在 [2,n]。那么后面每次操作都可以用这个零,因此全局都是最优的。最后排序,时间复杂度 O(n \log n)

Code

    a[0] = 0x3f3f3f3f;
	
	int i = 0, p1 = 0, p2 = 0;
	while (t--) {
		i++;
		if (a[i] < a[p1]) {p2 = p1; p1 = i;}
		else if (a[i] < a[p2]) {p2 = i;}
	}
	
	a[p1] += a[p2], a[p2] = 0;
	
	sort(a + 1, a + n + 1);
	
	for (int i = 1; i <= n / 2; i++) {
		ans += a[i];
	}

T2 邦布照相

蒟蒻不会单调栈啊。。。想出是栈结构,但是不会实现。于是蒟蒻换了种思路:

遍历每个元素,尝试选择每个型号的第一次出现,并在剩余次数较多时跳过。

Code

    for (int i = 1; i <= n; i++) {
		cin >> a[i];
		cnt[a[i]]++;
	}
	
	for (int i = 1; i <= n; i++) {
		if (vis[a[i]]) continue;
		if (a[i] > k) continue;
		else if (a[i] != 1 && cnt[a[i]] > 1) {cnt[a[i]]--; continue;}
		else {
			cout << a[i] << ' ';
			vis[a[i]] = 1;
		}
	}

然而,这种方法没有考虑字典序的最小化,导致无法在后续元素中找到更优的排列。

考虑这组 hack:
input

20 9
4 5 2 1 6 7 2 1 3 4 5 9 8 1 6 2 7 9 1 8

output

1 2 3 4 5 6 7 9 8 

它输出了:

1 3 4 5 6 2 7 9 8 

问什么错了?原因有以下几点:

  1. 贪心策略不当,代码仅根据当前元素是否首次出现以及剩余次数决定是否选择,未比较元素大小。例如,较大的元素被提前选中后,即使后面有更小的元素也无法替换,导致字典序偏大。
  2. 无法回溯调整,一旦元素被选中,就无法在后续步骤中调整顺序,而正确的字典序可能需要将前面的较大元素替换为后面的较小元素。

我们应该采用贪心策略,维护一个栈,每次遇到新元素时,尽可能将较小的元素放到前面:
用栈结构保存当前已选的元素,保持字典序尽可能小。当新元素比栈顶小且栈顶元素在后面还会出现时,弹出栈顶元素。记录每个元素的剩余可用次数,确保后续有足够元素组成答案。

考虑单调栈实现,时间复杂度 O(n)

Code

    for (int i = 0; i < n; ++i) {
        cin >> a[i];
        cnt[a[i]]++;
    }

    for (int i = 0; i < n; ++i) {
        int x = a[i];
        if (x > k || inStack[x]) {
            cnt[x]--;
            continue;
        }
        while (!st.empty() && x < st.top() && 
               cnt[st.top()] > 0 && k - st.size() + 1 <= n - i) {
            inStack[st.top()] = false;
            st.pop();
        }
        if (st.size() < k) {
            st.push(x);
            inStack[x] = true;
        }
        cnt[x]--;
    }

    vector<int> res;
    while (!st.empty()) {
        res.push_back(st.top());
        st.pop();
    }

    reverse(res.begin(), res.end());

    for (int v : res) cout << v << ' ';

    return 0;
}

T3 CQOI余数之和
把题意转化为公式:
给出正整数 nk,计算

j(n, k) = \sum_{i = 1}^n k \bmod i

其中 k\bmod i 表示 k 除以 i 的余数。

设答案为 ans,于是 ans = \sum_{i=1}^n k \bmod i

\because a\bmod b = a - b \times \lfloor \frac{a}{b} \rfloor
\therefore ans = \sum_{i=1}^n k - i \times \lfloor \frac{k}{i} \rfloor = n \times k - \sum_{i = 1}^n i - \lfloor \frac{k}{i} \rfloor

时间复杂度 O(\sqrt k)

Code

    if (n == 1) cout << 0, exit(0);
	else if (n == 2 && k & 1) cout << 1, exit(0);
	else if (n == 2 && !(k & 1)) cout << 0, exit(0); // 这几行不要也没关系
	
	int ans = 0, i = 0, j = 0;
	int m = n;
	
	while (1) {
		if (i == 0) i++;
		else i = j + 1;
		
		if (i > n) break;
		j = ((k / i == 0) ? n : min(n, k / (k / i)));
		ans -= (k / i) * (j - i + 1) * (i + j) / 2;
	}
	
	cout << ans + n * k;

越来越觉得自己像数学家了 OAO。当然论数学,肯定不如 @我命由我不由天
T4 集邮比赛 3

JOI 君在环形湖周围收集邮戳,每个邮戳台有位置和撤销时间,需在时间允许下收集最多邮戳。关键在于如何选择移动路径,使得收集的邮戳数量最大,同时保证到达每个邮戳台的时间不超过其撤销时间。

赛时写了小记搜,试图通过逐次移动相邻邮戳台并记录已收集的邮戳来求解,但是 Wa & Re

int dfs(int i, int cnt) {
	if (cnt == T) return (t[i] <= cnt && !vis[i]);
	if (cnt > T) return 0;
	if (dp[i][cnt]) return dp[i][cnt];
	int sum = 0;
	if (t[i] <= cnt && !vis[i]) sum = 1;
	
	vis[i] = 1;
	if (i == 1) {
		sum += max(dfs(i + 1, cnt + (x[i + 1] - x[i])), dfs(n, cnt + (x[1] + l - x[n])));
	} else if (i == n) {
		sum += max(dfs(i - 1, cnt + (x[i] - x[i - 1])), dfs(1, cnt + (x[1] + l - x[n])));
	} else {
		sum += max(dfs(i + 1, cnt + (x[i + 1] - x[i])), dfs(i - 1, cnt + (x[i] - x[i - 1])));
	}
	
	vis[i] = 0;
	
	return dp[i][cnt] = sum;
} 

仔细一看,问题还挺多的:
状态缺失了,记忆化状态仅记录当前位置和时间,未考虑已收集的具体邮戳,导致不同收集路径的状态被错误合并。
移动受限,只能移动到相邻邮戳台,无法跨过中间台选择更优路径,可能增加不必要的时间消耗。
未正确处理环形路径的最短距离,导致时间计算错误。

我们换种定义状态的方式,用 dp_{i,j,d,m},表示已收集左侧 i 个和右侧 j 个邮戳,当前位置在左或右端 d=0/1,已收集 m 个时的最小时间。通过状态转移,每次选择扩展左或右的下一邮戳台,计算最短时间并更新状态。

状态转移:

  • 从左侧 i 扩展至 i+1:计算顺时针移动时间,判断是否能在撤销时间内收集。
  • 从右侧 j 扩展至 j-1:计算逆时针移动时间,同理更新状态。

最后采用贪心策略,每次选择时间最短的路径扩展,确保在有限时间内收集尽可能多的邮戳。

Code

    dp[0][n + 1][0][0] = dp[0][n + 1][1][0] = 0; // 初始状态
    
    for (int i = 0; i < n; i++)
        for (int j = n + 1; j >= i + 2; j--)
            for (int m = 0; m <= n; m++) {
                if (dp[i][j][0][m] == inf && dp[i][j][1][m] == inf) continue;
                
                // 扩展左侧i+1
                ll cost0 = dp[i][j][0][m] + (x[i + 1] - x[i]); // 顺时针移动
                ll d1 = x[i + 1] - x[j];
                ll cost1 = dp[i][j][1][m] + min(abs(d1), l - abs(d1)); // 最短路径
                ll tmp = min(cost0, cost1);
                int nm = m + (tmp <= t[i + 1]);
                if (nm <= n) dp[i + 1][j][0][nm] = min(dp[i + 1][j][0][nm], tmp);
                
                // 扩展右侧j-1
                ll d2 = x[j - 1] - x[i];
                ll cost2 = dp[i][j][0][m] + min(abs(d2), l - abs(d2));
                ll cost3 = dp[i][j][1][m] + (x[j] - x[j - 1]);
                tmp = min(cost2, cost3);
                nm = m + (tmp <= t[j - 1]);
                if (nm <= n) dp[i][j - 1][1][nm] = min(dp[i][j - 1][1][nm], tmp);
            }
}

放上蔡老师为我们讲解的笔记:

/* dp[l][r][k][m] 
l往右走的到达的地点 l+1 
r往左走到达的地点 
k当前处于左边还是右边 
m代表当前收集到的站点数量 
最开始 dp[0][n+1][0][0] = 0; dp[0][n+1][1][0] = 0; 
状态转移: 
	站在右边dp[0][n+1][0][0] 
		往右边走(x[i+1]) dp[i][j][0][m] -> dp[i+1][j][0][m] { dp[i][j][0][m] + x[i+1]-x[i] } 
		往左走(x[j-1]) dp[i][j][0][m] -> dp[i][j-1][1][m] {dp[i][j][0][m] + l - x[j-1] + x[i]} 
					//变成站在右边 
	站在左边dp[0][n+1][1][0] 
		往左边走(x[j-1]) dp[i][j][1][m] -> dp[i][j-1][1][m] {dp[i][j][1][m] + x[j] - x[j-1]} 
		往右边走(x[i+1]) dp[i][j][1][m] -> dp[i+1][j][0][m] {dp[i][j][1][m] + l - x[i+1] + x[j]} 
9 个赞

一定要点赞啊 qwq

1 个赞

赞了

1 个赞