CF刷题——2500难度的几道题

1.D. Beautiful numbers

  • 题意:Beautiful Numbers定义为这个数能整除它的所有位上非零整数。问[l,r]之间的Beautiful Numbers的个数。
  • 数位 DP: 数位 DP 问题往往都是这样的题型,给定一个闭区间 [ l , r ] [l,r] [l,r],让你求这个区间中满足某种条件的数的总数。我们将问题转化成更加简单的形式。设 a n s i ans_i ansi 表示在区间 中满足条件的数的数量,那么所求的答案就是 a n s r − a n s l − 1 ans_{r} - ans_{l - 1} ansransl1.
  • 看代码吧,可以发现所有个位数的最小公倍数是2520,dfs(len, sum, lcm, limit) ,len表示迭代的长度,sum为截止当前位的数对2520取余后的值。 lcm为截止当前位的所有数的最小公倍数。limit表示当前数是否可以任意取值(对取值上限进行判断).
  • 代码其实很好懂,但是方法确实不是那么好想。
#include
#include
#include
using namespace std;
const int maxlcm = 2520;
typedef long long ll;
ll dp[20][50][2530];
int Hash[2530];
ll nums[20];
ll gcd(ll a, ll b) {
	if (b == 0) return a;
	return gcd(b, a % b);
}
ll dfs(ll pos, ll sum, ll lcm, bool limit) {
	if (pos == -1) {
		return sum % lcm == 0;
	}
	if (limit == false && dp[pos][Hash[lcm]][sum] != -1) return dp[pos][Hash[lcm]][sum];
	ll ans = 0;
	int up = limit ? nums[pos] : 9;
	for (int i = 0; i <= up; i++) {
		ans += dfs(pos - 1, (sum * 10 + i) % maxlcm, i ? i * lcm / gcd((ll)i, lcm) : lcm, limit && i == up);
	}
	if (limit == 0) dp[pos][Hash[lcm]][sum] = ans;
	return ans;
}
ll solve(ll N) {
	int p = 0;
	while (N) {
		nums[p++] = N % 10;
		N /= 10;
	}
	return dfs(p - 1, 0, 1, true);
}
int main() {
	int T;
	scanf("%d", &T);
	memset(dp, -1, sizeof dp);
	int cnt = 0;
	for (int i = 1; i <= maxlcm; i++) {
		if (maxlcm % i == 0) Hash[i] = cnt++;
	}
	while (T--) {
		ll l, r;
		cin >> l >> r;
		cout << solve(r) - solve(l - 1) << endl;
	}
	return 0;
}

2.C. Jzzhu and Apples

  • 题意:有N个苹果,编号为1~N。现要将苹果组成若干对,每对苹果最大公约数大于1。求最多能分成多少对,输出各对的组成。
  • 思路:枚举公约数d,只枚举素数,因为合数的可以在更小的素数被枚举。将所有没用过并且编号为d的倍数的苹果拿出来,两两组队,如果个数为奇数,那么就将2d留出来。因为d>2,所以第2个肯定是2d。并且2d是2的倍数。最后把这些2的倍数两两配对即可。
#include
#include
#include
#include
using namespace std;
const int maxn = 100010;
int st[maxn], prime[maxn], cnt;
void sieve(int N) {
	for (int i = 2; i <= N; i++) {
		if (!st[i]) prime[cnt++] = i;
		else {
			for (int j = 0; prime[j] <= N / i; j++) {
				st[prime[j] * i] = true;
				if (i % prime[j] == 0) break;
			}
		}
	}
}
int N;
bool vis[maxn];
typedef pair<int, int> P;
vector<P> ans;
int main() {
	sieve(maxn - 1);
	scanf("%d", &N);
	for (int i = 1; i < cnt; i++) {
		int p = prime[i];
		vector<int> res;
		for (int j = p; j <= N; j += p) {
			if (!vis[j]) res.push_back(j);
		}
		int num = res.size();
		if (num == 1) continue;
		if (num & 1) {
			ans.push_back(P(res[0], res[2]));
			vis[res[0]] = vis[res[2]] = true;
			for (int j = 3; j < num; j += 2) {
				ans.push_back(P(res[j], res[j + 1]));
				vis[res[j]] = vis[res[j + 1]] = true;
			}
		}
		else {
			for (int j = 0; j < num; j += 2) {
				ans.push_back(P(res[j], res[j + 1]));
				vis[res[j]] = vis[res[j + 1]] = true;
			}
		}
	}
	for (int i = 2; i <= N; i += 2) {
		if (vis[i]) continue;
		int p1 = i;
		i += 2;
		while (i <= N && vis[i]) i += 2;;
		if(i <= N && !vis[i]) ans.push_back(P(p1, i));
	}
	printf("%d\n", ans.size());
	for (auto p : ans) {
		printf("%d %d\n", p.first, p.second);
	}
	return 0;
}

3.D. Fuzzy Search

  • FFT
  • 题意:给定主串和模式串,字符集大小为4,给定k,模式串在某个位置匹配当且仅当任意位置模式串的这个字符所对应的母串的位置的左右k个字符之内有一个与它相同的,求匹配次数。
  1. 分别计算ACTG四个字母。
  2. 若主串在第 i i i 个位置的 [ i − k , i + k ] [i-k, i + k] [ik,i+k] 范围内有某个字母,就把这个位置记为1,否则记为0,新得到的这个数组记为A. 然后,在模式串中,若第 i i i 个是该字母,记为1,否则记为0,得到的这个新数组记为B。
  3. 那么,我们计算 A 与 B 的卷积,那么第 i 个位置的值,就是若把主串放在第 i 个位置,那么卷积的第 i 项的值就是主串中能匹配到模式串该字母的数量。把这四个字母的四次卷积结果相加。
  4. 若最终第 i 项的值为模式串的长度,那么说明这个位置可以放该字母。
#include
#include
#include
#include
#include
using namespace std;
typedef long long ll;
typedef complex<double> Comp;
const int maxn = 800010;
int n, m, N, M, K, L;
char S[maxn], T[maxn];
Comp A[maxn], B[maxn];
int vis[5][maxn], cnt[5], id[256], ans[maxn], rev[maxn];
const double PI = 2 * asin(1);
void FFT(Comp* a, int f) {
	for (int i = 0; i < n; i++) if (rev[i] > i) swap(a[i], a[rev[i]]);
	for (int i = 1; i < n; i <<= 1) {
		Comp wn(cos(PI / i), sin(PI / i));
		for (int p = i << 1, j = 0; j < n; j += p) {
			Comp w(1, 0);
			for (int k = 0; k < i; k++, w *= wn) {
				Comp x = a[k + j], y = w * a[k + j + i];
				a[k + j] = x + y; a[k + j + i] = x - y;
			}
		}
	}
	if (f == -1) reverse(a + 1, a + n);
}
int main() {
	scanf("%d%d%d", &N, &M, &K);
	scanf("%s%s", S + 1, T + 1);
	//计算数组
	id['A'] = 1, id['G'] = 2, id['T'] = 3, id['C'] = 4;
	for (int l = 0, r = 0, i = 1; i <= N; i++) {
		while (l < N && l < i - K) cnt[id[S[l++]]]--;
		while (r < N && r < i + K) cnt[id[S[++r]]]++;
		for (int j = 1; j <= 4; j++) if (cnt[j]) vis[j][i] = 1;
	}
	m = N + M;
	for (n = 1; n <= m; n <<= 1) L++;
	for (int i = 0; i < n; i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (L - 1));
	for (int k = 1; k <= 4; k++) {
		memset(A, 0, sizeof A);
		memset(B, 0, sizeof B);
		for (int i = 1; i <= N; i++) {
			if (vis[k][i] == 1) A[i - 1] = 1;
		}
		for (int i = 1; i <= M; i++) {
			if (id[T[i]] == k) B[M - i] = 1;
 		}
		FFT(A, 1), FFT(B, 1);
		for (int i = 0; i < n; i++) A[i] *= B[i];
		FFT(A, -1);
		for (int i = 0; i < n; i++) ans[i] += (ll)(A[i].real() + 0.5) / n;
	}
	int res = 0;
	for (int i = 0; i < n; i++) if (ans[i] == M) res++;
	printf("%d\n", res);
	return 0;
}

4.F. Kuroni and the Punishment

  • 题意:有一个序列,n个数字,一个操作,可以把这个序列的某个数字加1或者减1。问至少需要多少次操作,可以让这个序列所有数字的最大公约数大于1.
  • 随机数的题目。我们可以O(n)求出,调整最后gcd为素数P的倍数的最少操作数。问题是这个P。由于素数2的存在,我们可以得出操作数至多为n。
  • 如果最优的方案,要进行>=2次的操作的数字的数量>n/2,那最终答案显然>n,因此不可能为最优解,得出结论:最优方案中,只要进行<2(0或1)次操作的数的个数>=n/2。
  • 至少存在n/2个数,其最后变为 i , i + 1 , i − 1 i , i + 1 , i − 1 i,i+1,i1 ( n/2个加减2,n/2个不动 ). 也就是说,随便从 n n n 个数里面选择一个数 a i , a i + 1 , a i − 1 a_i, a_i + 1, a_i - 1 ai,ai+1,ai1 的因子中存在最后答案素数P的概率为1/2。
  • 官方题解说,选20次足够。但我循环了50次。
#include
#include
#include
#include
#include
#include
using namespace std;
typedef long long ll;
const int maxn = 200010;
ll a[maxn];
int N;
vector<ll> divisor(ll N) {
	vector<ll> res; 
	for (ll i = 2; i <= N / i; i++) {
		if (N % i == 0) {
			res.push_back(i);
			while (N % i == 0) N /= i;
		}
	}
	if (N > 1) res.push_back(N);
	return res;
}
unordered_set<ll> tried;
int main() {
	scanf("%d", &N);
	for (int i = 0; i < N; i++) scanf("%lld", &a[i]);
	//srand(NULL);
	ll ans = 1e9;
	
	for (int i = 0; i < 50; i++) {
		unordered_set<ll> primes;
		int id = rand() * rand() % N;
		//printf("*** %d\n", id);
		for (ll j = -1; j <= 1; j++) {
			if (a[id] + j == 0LL) continue;
			vector<ll> v = divisor(a[id] + j);
			for (auto u : v) primes.insert(u);
		}
		if(i == 0) primes.insert(2);
		for (auto p : primes) {
			ll res = 0;
			if (tried.count(p)) continue;
			tried.insert(p);
			for (int j = 0; j < N; j++) {
				if (a[j] < p) res += p - a[j];
				else {
					res += min(a[j] % p, p - (a[j] % p));
				}
			}
			//printf("### %lld %lld\n", p, res);
			ans = min(res, ans);
		}
	}
	printf("%lld\n", ans);
	return 0;
}

5.F. Make It One

  • 题意:这个题意真是简单。给一个序列,取出一个子集,使得他们最大公约数是1,问这个子集大小最小是多少。
  • 前7个最小的素数之积大于300000,因此答案最大是7.
  • Let’s define d p [ i ] [ j ] dp[i][j] dp[i][j] — number of ways to pick i different elements such that their gcd is equal to j. 当然,不可以是2j, 3j…
  • d p [ i ] [ j ] = C c n t j i − ∑ k = 2 ∞ d p [ i ] [ k ∗ j ] dp[i][j] = C_{cnt_j}^{i} - \sum\limits_{k = 2}^{\infty}dp[i][k*j] dp[i][j]=Ccntjik=2dp[i][kj]. c n t j cnt_j cntj 代表 j ∣ a i j|a_i jai 的 j 的数量。
  • 不过要小心,结果可能非常大,需要模一个大素数。
#include
#include
#include
using namespace std;
typedef long long ll;
const ll mod = 1e9 + 7;
const int maxn = 300010;
int cnt[maxn], mx, N;
ll dp[10][maxn], fact[maxn], infact[maxn];
ll mod_pow(ll x, ll n) {
	ll res = 1;
	while (n) {
		if (n & 1) res = res * x % mod;
		x = x * x % mod;
		n >>= 1;
	}
	return res;
}
ll C(ll a, ll b) {
	if (a < b) return 0;
	return fact[a] * infact[b] % mod * infact[a - b] % mod;
}
int main() {
	fact[0] = infact[0] = 1;
	for (ll i = 1; i <= 300000; i++) {
		fact[i] = fact[i - 1] * i % mod;
		infact[i] = infact[i - 1] * mod_pow(i, mod - 2) % mod;
	}
	scanf("%d", &N);
	for (int i = 0; i < N; ++i) {
		int x;
		scanf("%d", &x);
		++cnt[x];
		mx = max(mx, x);
	}
	for (int i = 1; i <= mx; ++i) {
		for (int j = 2 * i; j <= mx; j += i) {
			cnt[i] += cnt[j];
		}
	}
	for (int i = 1; i <= 7; i++) {
		for (int j = mx; j >= 1; j--) {
			dp[i][j] = C(cnt[j], i);
			for (int k = 2; k * j <= mx; k++) {
				dp[i][j] = (dp[i][j] - dp[i][k * j] + mod) % mod;
			}
		}
		if (dp[i][1]) {
			printf("%d\n", i);
			return 0;
		}
	}
	printf("-1\n");
	return 0;
}

6.F. Ray in the tube

  • 给一个导管,导管内壁可以反射激光,导管内壁一些位置分布着感应器。现在在导管中投射一束激光,问最多有多少个感应器可以接受到激光。
  • 其实,很容易发现,所有步长是奇数的情况都可以被步长为1的取代。因此只需暴力枚举2的幂既可。
  • 有两个地方容易错。一个是步长为0时,要判断一下。另一个是,没必要一定要选第一个点。而既然每次增长步数u是2的幂,因次再导管内壁的一侧,位置模2*u相同的地方是可以同时照到的。
#include
#include
#include
using namespace std;
const int maxn = 100010;
typedef long long ll;
typedef unordered_map<ll, int> um;
um ma, mb, c1, c2;
int N, M, Y1, Y2;
int main() {
	scanf("%d%d", &N, &Y1);
	ll x;
	int cntmax_a = 0, cntmax_b = 0;
	//因为增长步数可以是0,所以底下是特判。
	for (int i = 0; i < N; i++) {
		scanf("%lld", &x);
		++ma[x];
		cntmax_a = max(cntmax_a, ma[x]);
	}
	scanf("%d%d", &M, &Y2);
	for (int i = 0; i < M; i++) {
		scanf("%lld", &x);
		++mb[x];
		cntmax_b = max(cntmax_b, mb[x]);
	}
	int ans = cntmax_a + cntmax_b;
	for (ll u = 1; u <= 1000000000; u <<= 1) {
		c1.clear(), c2.clear();
		for (auto p : ma) {
			c1[p.first % (2 * u)] += p.second;
		}
		for (auto p : mb) {
			c2[p.first % (2 * u)] += p.second;
		}
		for (auto p : c1) {
			ans = max(ans, p.second + c2[(p.first + u) % (2 * u)]);
		}
		for (auto p : c2) {
			ans = max(ans, p.second + c1[(p.first + u) % (2 * u)]);
		}
	}
	printf("%d\n", ans);
	return 0;
}

7.F. Anton and School

在这里插入图片描述

  • 题意:给出数组 b b b 和数组 c c c,请构造出数组 a a a.
  • 思路:发现 a & b + a ∣ b = a + b a\&b + a|b=a+b a&b+ab=a+b.
  • s u m a = ∑ ( b i + c i ) ( 2 n ) , a i = b i + c i − s u m n . sum_a = \frac{\sum(b_i+c_i)}{(2n)}, a_i = \frac{b_i + c_i - sum}{n}. suma=(2n)(bi+ci),ai=nbi+cisum.
  • check的时候,带入回去看看。我发现,我经常碰到复杂度是 O ( n 2 ) O(n^2) O(n2) 的算法,但是,如果从每一个数的贡献,通常可以优化到 O ( n l o g n ) O(nlogn) O(nlogn) O ( n ) O(n) O(n),而且, O ( n l o g n ) O(nlogn) O(nlogn) 通常是计算对每一个数位的贡献。
#include
#include
#include
using namespace std;
const int maxn = 200010;
typedef long long ll;
ll N, a[maxn], b[maxn], c[maxn], cnt[40];
bool check() {
	for (int i = 1; i <= N; i++) {
		ll ckb = 0, ckc = 0;
		for (int j = 0; j <= 30; j++) {
			if ((a[i] >> j) & 1) {
				ckb += cnt[j] * (1LL << j);
				ckc += (1LL << j) * N;
			}
			else {
				ckc += cnt[j] * (1LL << j);
			}
		}
		if (ckb != b[i] || ckc != c[i]) return false;
	}
	return true;
}
int main() {
	scanf("%lld", &N);
	ll sumbc = 0;
	for (int i = 1; i <= N; i++) {
		scanf("%lld", &b[i]);
		sumbc += b[i];
	}
	for (int i = 1; i <= N; i++) {
		scanf("%lld", &c[i]);
		sumbc += c[i];
	}
	ll suma = sumbc / 2 / N;
	for (int i = 1; i <= N; i++) {
		a[i] = (b[i] + c[i] - suma) / N;
		for (int j = 0; j <= 30; j++) {
			cnt[j] += ((a[i] >> j) & 1);
		}
	}
	if (check()) {
		for (int i = 1; i <= N; i++) printf("%lld%c", a[i], i == N ? '\n' : ' ');
	}
	else printf("-1\n");
	return 0;
}

8.C. On the Bench

题解1
题解2
题解3

  • 题意:给定n (1≤n≤300) 个数,求问有多少种排列方案使得任意两个相邻的数之积都不是完全平方数。由于方案数可能很大,输出方案数 mod 1 0 9 + 7 10^9+7 109+7 的值。
  • 首先我们对n个数进行一个分组,把乘积为平方数的数分在一组(可以证明:同一组内的数两两乘积为平方数,不同组的两个数乘积一定不是平方数),记作一共分了nn组,则同组的数只要不相邻即可满足题意。即原问题转化为 n个物品,分成了nn组,要求同组物品不能相邻,问共有几种排列方案。
  • 问题抽象:设集合 A 有 n 1 n_1 n1 a 1 a_1 a1, n 2 n_2 n2 a 2 a_2 a2,…, n i n_i ni a i a_i ai 问使其相邻两个数不相同的排列有多少种.这个问题被称为多重集合交错排列。
  • 运用插空法最好可以做到 O ( n 3 ) O(n^3) O(n3)。但是这个做法已经过时了。新的方法是 O ( n 2 ) O(n^2) O(n2)
  • 限制是相邻两个球不能相同。当违反限制的时候,可以看成相邻两个球粘在了一起。
  • d p [ i ] [ k ] dp[i][k] dp[i][k] 表示考虑前 i个数分成 k 块的个数,那么,
    d p [ i ] [ k ] = ∑ j = 1 k d p [ i − 1 ] [ k − j ] C n i − 1 j − 1 ∗ n i ! j ! dp[i][k]=∑_{j=1}^kdp[i−1][k−j]C_{n_i−1}^{j−1}∗\frac{n_i!}{j!} dp[i][k]=j=1kdp[i1][kj]Cni1j1j!ni!
  • d p [ 0 ] [ 0 ] = 1. dp[0][0] = 1. dp[0][0]=1.
  • d p [ k ] ∗ k ! dp[k]∗k! dp[k]k! 表示的是有 n−k个数相邻, a n s = d p [ n ] n ! − d p [ n − 1 ] ( n − 1 ) ! + ⋯ + ( − 1 ) ( n − i ) d p [ i ] i ! ans=dp[n]n!−dp[n−1](n−1)!+⋯+(−1)^{(n−i)}dp[i]i! ans=dp[n]n!dp[n1](n1)!++(1)(ni)dp[i]i!
#include
#include
#include
#include
#include
#include
using namespace std;
const int maxn = 310;
typedef long long ll;
ll dp[maxn][maxn], N, fact[maxn], infact[maxn];
const ll mod = 1e9 + 7;
ll mod_pow(ll x, ll n) {
	ll res = 1;
	while (n) {
		if (n & 1) res = res * x % mod;
		x = x * x % mod;
		n >>= 1;
	}
	return res;
}
unordered_map<ll, ll> tot;
vector<ll> cnt;
ll C(ll m, ll n) {
	return fact[m] * infact[n] % mod * infact[m - n] % mod;
}
int main() {
	fact[0] = infact[0] = 1;
	for (ll i = 1; i < maxn; i++) {
		fact[i] = i * fact[i - 1] % mod;
		infact[i] = infact[i - 1] * mod_pow(i, mod - 2) % mod;
	}
	dp[0][0] = 1;
	scanf("%lld", &N);
	for (int i = 0; i < N; i++) {
		ll x;
		scanf("%lld", &x);
		bool flag = false;
		for (auto p : tot) {
			ll u = p.first;
			ll res = (ll)(sqrt(u * x) + 0.5);
			if (u * x == res * res) {
				flag = true;
				tot[u]++;
				break;
			}
		}
		if (!flag) tot[x]++;
	}

	for (auto p : tot) {
		cnt.push_back(p.second);
	}
	int sz = tot.size();

	dp[0][0] = 1;
	for (int i = 1; i <= sz; i++) {
		for (int k = 1; k <= N; k++) {
			for (int j = 1; j <= min((int)cnt[i - 1], k); j++) {
				dp[i][k] = (dp[i][k] + dp[i - 1][k - j] * C(cnt[i - 1] - 1, j - 1LL) % mod
					* fact[cnt[i - 1]] % mod * infact[j] % mod) % mod;
			}
		}
	}
	ll ans = 0;
	for (ll i = N; i >= 0; i--) {
		ll f = ((N - i) & 1) ? -1LL : 1LL;
		ans +=  f * dp[sz][i] % mod * fact[i] % mod;
		ans = (ans % mod + mod) % mod;
	}
	printf("%lld\n", ans);
	return 0;
}

你可能感兴趣的:(ACM题目整理,动态规划,深度优先,算法)