CSP模拟-4

日,怎么第一天考试直接4道思维题,被真实力..........

T1 [ARC125C] LIS to Original Sequence

这道题还是比较简单的 能想到

由于题目里面让求字典序最小,因此我们可以隐约的想到做法:贪心。

我们现在将一个 \(1\)\(n\) 的数列分成输入的数和除输入以外的数。然后我们再将 \(a_k(k>1)\) 的最长上升子序列分成两个部分:\(a_1,a_2,a_3,\dots,a_{k-1}\)\(a_k\)

Part 1(第一个序列)

对于一个数 \(x\),如果它小于 \(a_i\),则 \(x\) 一定不可以放在 \(a_i\) 前面,因为它会造成最长上升子序列的长度增大,因此我们只能把它放在 \(a_i\) 后面序列。

如果我们放多个比 \(a_i\) 小的数在 \(a_i\) 后面,让它们顺序排,会使最长上升子序列变长;如果我们让它们逆序排,不符合字典序最小的要求,所以只能放一个。

接下来对 \(a_1\)\(a_{k-1}\) 进行操作即可。

Part 2(第二个序列)

先来说说我们为什么分成两个序列。我们如果对于 \(a_k\) 依照上面处理的方式处理的话。我们后面剩下的数该怎么处理呢?\(a_k\) 后面的数不可能再比 \(a_k\) 大,那样的话会使最长上升子序列的长度变长,所以 \(a_k\) 后面的数一定要比 \(a_k\) 的值小。这样,我们处理后一个序列的方式就出现了。将我们插剩下的数与 \(a_k\) 进行排序,逆序输出就行。

Code

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>

using namespace std;

int n, k, a[5211314];
int num[5211314], sum, pos = 1;

int main() {
	cin >> n >> k;
	for (int i = 1; i <= k; ++ i) {
		cin >> a[i];
	}
	for (int i = 1, now = 1; i <= n; ++ i) {
		if (i != a[now]) {
			num[++ sum] = i;
			//将不是输入的数从小到大存起来 
		}
		else now ++;
	}
	num[sum + 1] = 1e9;//防止在下面的if里一直输出0 
	for (int i = 1; i <= k - 1; ++ i) {
		cout << a[i] << " ";
		if (num[pos] < a[i]) {
			//找不在输入序列里第一个比a[i]小的数,插入到前面
			//可以得到字典序最小 
			cout << num[pos] << " ";
			pos ++;
		}
	}
	if (pos <= sum) {
		//特殊处理a[k] 
		for (int i = sum; i >= pos; -- i) {
			if (a[k] > num[i]) {
				cout << a[k] << " ";
				a[k] = -1;
			}
			cout << num[i] << " ";
		}
		if (a[k] != -1) {
			cout << a[k] << endl;
		}
		//将剩下的数从大到小输出 
	}
	else {
		cout << a[k];
		//只剩下a[k]的情况特殊看 
	}
	return 0;
}

T2 [ARC125D] Unique Subsequence

这边建议这篇[题解]([ARC125D] Unique Subsequence - int_R の blog - 洛谷博客 (luogu.com.cn))

但这篇巨佬的题解里有一些我认为的错误,在代码里面标出来了

#include <iostream>
#include <cmath>
#include <cstring>
#include <algorithm>
#include <cstdio>
#define mod 998244353

using namespace std;

long long n, a[521314], same[5211314], las[5211314];
long long tree[5211314], f[5211314];

class BinaryIndexedTree {
	
	private:
		int lowbit(int x) {
			return (x & (-x));
		}
	public:
		void Add(int x, int num) {
			while (x <= n) {
				tree[x] = (tree[x] + num) % mod;
				x += lowbit(x);
			}
		}
		long long Query(int x) {
			long long ans = 0;
			while (x != 0) {
				ans = (ans + tree[x]) % mod;
				x -= lowbit(x);
			}
			return ans;
		}
}Tree;



int main() {
	cin >> n;
	for (int i = 1; i <= n; ++ i) {
		cin >> a[i];
		same[i] = las[a[i]];
		las[a[i]] = i;
	} 
	for (int i = 1; i <= n; ++ i) {
		if (same[i] != 0) {
			f[i] = (Tree.Query(i - 1) - Tree.Query(same[i] - 1) + mod) % mod;
			//这里不能将same[i]前面的方案数加上.
			//i只能在same[i]到i-1转移,因为从1,pre-1中转移到i的也可以转移到same[i]所以代表取出方案并不唯一 
			//注意 这里找same[i]-1 是因为same[i]可以组成的方案后面加上a[i]也不会重复 要加上 
			//如a[5]=4,same[5]=4,若f[same[i]]有 1 2 3 4的方案.则f[i]有方案 1 2 3 4 4,不重复 
			//而不是因为same[i]取得方案与a[i]可以取得方案有重复才取到same[i]-1 
			Tree.Add(same[i], -f[same[i]]);
			//此处题解可能理解错了 
			//将f[same[i]]的值赋成0。因为a[i]与same[i]重复了,不能将same[i]的方案数加上 
			/*若存在j>i,则j不能从same[i]转移,因为same[i]与a[i]重复了,所以不能取same[i]的方案。
			  而是要取same[i]的方案后面加上a[i]的方案*/ 
		}
		else {
			f[i] = (Tree.Query(i - 1) + 1);
			//这里的加1是将它本身为一个单独子串的方案数加上 
		}
		Tree.Add(i, f[i]);
	}
	cout << (Tree.Query(n) + mod) % mod << endl;
	return 0;
} 

T3 [ARC126C] Maximize GCD

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>

using namespace std;

long long n, k, a[5211314], maxn; 
//maxn表示输入的最大的数 
long long barrel[5211314];
long long sum1[5211314], sum2[5211314], maxsum;
//sum1[i] 表示值1到i有几个数 sum2[i]表示值为1到i的数和 
//maxsum表示把每个数加到maxn的花费 

int main() {
	cin >> n >> k;
	for (long long i = 1; i <= n; ++ i) {
		cin >> a[i];
		maxn = max(maxn, a[i]);
		barrel[a[i]] ++;
	} 
	for (long long i = 1; i <= 2 * maxn; ++ i) {
		sum1[i] = sum1[i - 1] + barrel[i];
		sum2[i] = sum2[i - 1] + barrel[i] * i;
	}
	for (long long i = 1; i <= n; ++ i) {
		maxsum += (maxn - a[i]);
		//所有的数加到maxn的花费
	}
	if (maxsum <= k) {
		//看k能不能满足花费
		printf("%lld\n", maxn + (k - maxn) / n);
		return 0; 
	} 
	for (long long i = maxn; i >= 1; -- i) {
		//枚举因数
		maxsum = 0;
		for (long long j = 1; (j - 1) * i <= maxn; ++ j) {
			long long cnt;
			cnt = sum1[j * i - 1] - sum1[(j - 1) * i];
			//有多少个数在 ((j-1)*k, j*k] 的区间内
			maxsum += cnt * j * i - (sum2[j * i - 1] - sum2[(j - 1) * i]);
			//maxsum加的表示将上述区间内的数均加到j*k的花费
		}
		if (maxsum <= k) {
			printf("%lld\n", (long long)i);
			return 0;
		}
	}
	return 0;
} 

T4 [ARC126D] Pure Straight

我们这道题要用逆天的 dp 做(考场没想到),考试后也是请教了巨佬才明白 。

题意有很多题解都讲的很好,就不在这里过多赘述了。

看到 \(k<=16\) 的时候,我们可以想到用 \(2^k\) 的时间复杂度来做,因此就会自然的想到状态压缩 dp。

我们设 \(dp_{i,S}\) 表示转移到第 \(i\) 个元素时,我们选取元素所构成的集合为 \(S\),这时我们移动的次数为 \(dp_{i,S}\)

我们会有两种情况转移过来:选或者不选。

  • 选现在的 \(a_i\),那么此时加入集合的花费即为比 \(a_i\) 大的数的个数。但是可能 \(a_i\) 转移到的状态有其他状态转移过来的更优解,所以要取 min。

  • 若不选,我们要么将现在处理好的集合 \(S\) 移到现在选的元素 \(i\) 的后面,要么将未来可能会移过来的数的花费先加上,也就是先加上 \(S\) 的补集的数的个数。这个的大概意思就是说将两边已经排好序的集合不断的向中间靠拢,这样才能保证花费最小。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>

using namespace std;

int n, k, a[1000000], dp[1000000];

int main() {
	scanf("%d%d", &n, &k);
	for (int i = 1; i <= n; ++ i) {
		scanf("%d", &a[i]);
	}
	memset(dp, 0x3f, sizeof(dp));
	dp[0] = 0;
	for (int i = 1; i <= n; ++ i) {
		int mask = (1 << (a[i] - 1)) - 1, bit = (1 << (a[i] - 1));
		for (int j = (1 << k) - 1; j >= 0; -- j) {
			if ((j & bit) == 0) {
				dp[j | bit] = min(dp[j | bit], dp[j] + __builtin_popcount(j & (~mask)));
			}
			//若能选,就比较一下 
			dp[j] = dp[j] + min(__builtin_popcount(j), k - __builtin_popcount(j));
			//将不选的情况更新 至于为什么不是else 因为我们选了比较的情况下,我们也可以重新选择不选 
		}
	}
	cout << dp[(1 << k) - 1] << endl;
	return 0;
} 

总结

1.考试的时候一直跳题,所以一直没有想一道题,其实CSP-4模拟里除了T4 都可做。
2.第一次考试不太有

posted @ 2023-07-28 20:41  觉清风  阅读(15)  评论(0编辑  收藏  举报