【思维专练】专题训练 1

前言

思维训练 1,几乎没有什么算法,枚举 or 搜索 or 二分

  • CF1681D Required Length

\(\mathtt{TAGS}\):暴搜 + 剪枝

前置函数

下文中称:\(len(x)\)\(x\) 十进制下的位数。

First. 为什么是搜索

开始看到这道题想到了贪心:每次找出最大的一个数乘上去。

但是很显然:可以先乘一个小的数,使得其中某一位增大大,再乘上最大值是优于直接每次都乘最大值的。例如:\(42 \to 84 \to 672\)

所以,贪心无法做。

再看一眼数据范围,发现 \(n \le 19\),那么极端一点,对于 \(2\)
每次都只 \(\times 2\),最多也只需要 \(\log_2 10^{19}\) 次。

所以可以尝试一下搜索和剪枝。

Second.如何剪枝

  1. 可行性剪枝:如果一个数中没有任何一个数字 \(\ge1\),且位数 \(<n\),那么该数一定不能再增加,所以可以剪掉。

  2. 最优化剪枝:如果当前到达 \(x\) 的次数再加上 \(n - len(x)\)(即要使 \(x\) 的长度到达 \(n\) 最少需要的步数), \(\ge ans\) 那么,一定不是最优解。

  3. 最优化剪枝2:如果当前到达 \(x\) 的步数 \(\ge\) 之前到达 \(x\) 的步数,那么此时也不可能是最优解。

Third. 无解

如果 \(len(x)\) 怎样都到不了 \(n\) 那么无解。

Code

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
#define int long long
bool check (int x) {
	for (int p = x; p; p /= 10) {
		if(p % 10 > 1) return 1;
	}
	return 0;
}
int getlen (int x) {
	int len = 0;
	for (int p = x; p; p /= 10) len ++;
	return len;
}
int n, x;
int ans = 1e18;
map<int, int> res;
void dfs (int x, int cnt) {
	int len = getlen(x);
	if(!check(x) && len != n) return;
	if(!res[x]) res[x] = 1e18;
	if(cnt >= res[x] || cnt + (n - len) >= ans) return;
	res[x] = cnt;
	if(getlen(x) == n) {
		ans = min(ans, cnt);
		return;
	}
	for (int p = x; p; p /= 10) {
		int num = p % 10;
		if(num == 1 || num == 0) continue;
		dfs(x * num, cnt + 1);
	}
}

signed main() {
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin >> n >> x;
	dfs(x, 0);
	if(ans == 1e18) cout << -1;
	else cout << ans << endl;
	return 0;
}
  • CF1469D Ceil Divisions

\(\mathtt{TAG}\): 构造

First. \(2\) 的位置

首先,题目要求操作次数在 \(n + 5\) 以内,那么我们的构造的次数肯定是越小越好。

由于序列是一个排列,为了减少次数,不妨让初始的 \(2\) 即为最终的 \(2\),将其余数全部归 \(1\)

Second. 如何次数更小地形成 \(1\)

从最大数 \(n\) 开始考虑,我们希望尽可能地让这个数让越多数变成 \(1\),又得让剩余的数让它变成 \(1\) 的次数更小。

\(x\sim n - 1\) 为,\(n\) 的操作范围,那么操作次数即为:\(n - x + n \log_{x - 1}\)

\(x = \sqrt{n} + 1\) 时,该式取得最小值。

所以,对于 \(\sqrt{n} + 1 \sim n\) 的这一段区间需要的次数即为:\(n - \sqrt{n} + 2\),比区间长度多 \(1\)

我们只需要一直进行这个操作,使得 \(3 \sim n\) 这段区间全部归 \(1\)

而每次操作,量级缩小至 \(\sqrt{n}\),而达到 \(3\),极限范围最多只需要 \(4\) 次操作,所以,每次最多多使用 \(4\) 次,所以不会超出 \(n + 5\) 次,所以这种构造方式是可行的。

Code

#include <bits/stdc++.h>
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
int n;
signed main() {
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	int t;
	cin >> t;
	while(t --) {
		cin >> n;
		vector< pair<int, int> > ans;
		for (int i = n; i >= 3; i --) {
			int div = ceil(sqrt(i));
			for (int j = div + 1; j < i; j ++) ans.push_back({j, i});
			ans.push_back({i, div}), ans.push_back({i, div});
			i = div + 1;
		}
		cout << ans.size() << endl;
		for (auto opt : ans) cout << opt.first << ' ' << opt.second << endl;
	}
	return 0; 
}
  • CF1620C BA-String

\(\mathtt{TAG}\):枚举

First. 缩点

对于一段长度为 \(len\) 连续的 *,它的生成方案数不是 \((k + 1)^{len}\)。为什么?对于该段,长度至多为 \(k \times len\),至少为 \(0\),所以长度一共有 \(len\times k + 1\) 种方案,而所有字符都是相同的,所以方案数仅与长度有关。

于是,不妨将所有连续的 * 所为一个可以生成 \(0 \sim len \times k\) 的特殊 *,后面成为 \(cnt\)

Second. 定位

考虑字典序最小,肯定是优先展开靠后的 *

对于后 \(i\)*,他们可以生成的字符串的种类数为 \(fac_i = \prod_{j = i}^{n} cnt_j\)

找到第一个 \(< x\)\(fac_i\),记为 \(pos\)

而对于 \(pos - 1\),它每增加一个 b,就会增加 \(fac_{pos}\) 种字符串,所以 \(pos - 1\) 需要展开的个数,即为 \(\frac{x}{fac_{pos}}\)

对于剩下 \(pos\) 个数,它们应该是后 \(pos\)* 所形成的字符串种第 \(x \bmod fac_{pos}\) 个,按照这种方法接着扫下去,就可以得到每个 * 需要展开的次数。随后,输出即可。

Tips

  1. \(fac_i\) 可能爆 long long 所以求到第一个 \(> x\)\(fac_i\) 就可以停下了,而前面的 * 肯定不能展开。

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int , int> pi;
#define endl '\n'
#define int long long
const int N = 2000 + 10;
int n , k , x , tot , tmp;
string s;
int len[N] , cnt[N] , p[N] , fac[N] , to[N];
bool type[N];
int ans[N];

signed main() {
    ios::sync_with_stdio(0) , cin.tie(0) , cout.tie(0);
    int _;
    cin >> _;
    while (_--) {
        cin >> n >> k >> x >> s;
        x--;
        s = '.' + s;
        tot = tmp = 0;
        memset(type, 0, sizeof type), memset(len, 0, sizeof len), memset(fac, 0, sizeof fac);
        memset(cnt, 0, sizeof cnt), memset(ans, 0, sizeof ans);
        for (int i = 1; i <= n; i++) {
            if (s[i] != s[i - 1]) len[++tot] = 1 , type[tot] = (s[i] == '*');
            else len[tot]++;
        }
        for (int i = 1; i <= tot; i++) {
            if (type[i]) to[i] = ++tmp , cnt[tmp] = len[i] * k + 1;
        }
        fac[tmp + 1] = 1;
        for (int i = tmp; i >= 1; i--) {
            if(fac[i + 1] * 1.0 > (x * 1.0 / (cnt[i] * 1.0))) break; // 防止爆 Long long
            fac[i] = fac[i + 1] * cnt[i];
        }
        for (int i = 2; i <= tmp + 1; i++) {
            if(fac[i] == 0) continue;
            ans[i - 1] = x / fac[i];
            x %= fac[i];
        }
        for (int i = 1; i <= tot; i++) {
            if (!type[i]) {
                for (int j = 1; j <= len[i]; j++) cout << 'a'; // 对于连续段 a
            } else {
                for (int j = 1; j <= ans[to[i]]; j++) cout << 'b'; // 对于连续段 b
            }
        }
        cout << endl;
    }
    return 0;
}
  • CF1550C Manhattan Subarrays

\(\mathtt{TAG}\):暴力、BIT、组合数学

一开始震惊到我了,\(O(n^2 \log_2 n)\) 过了,然后一算发现是 \(O(n \log_2 n)\) 的。

First. 解析坏的三元组

画个图发现,当且仅当 \(x_1 \le x_2 \le x_3\)\(y_1 \ge y_2 \ge y_3\)\(y_1 \le y_2 \le y_3\) 的时候构成坏三元组。

回到题目上,由于每个数的横坐标是下标,所以坏三元组就转化为了一个长度为 \(3\) 不下降或不上升的子序列。

Second. 没有坏三元组的区间

首先不妨进行最暴力的想法,枚举每个区间,看它存不存在坏三元组。

而没有坏三元组的子数组的最长长度为 \(4\)

证明

如图,当两个同幅度的子序列相错的时候不能再添加任何一个点。

补充:正确证明是代入 Erdős–Szekeres 定理

Erdős–Szekeres 定理:对于 \(|S| = n \times m + 1\) 的偏序集 \((S,\succ)\) ,一定存在一条长度为 \(m + 1\) 的链或长度为 \(n + 1\) 的反链。

然后代入 \(m + 1 = 3\),得 \(|S| = 5\)
所以要是其不存在长度为 \(3\) 的链,那么 \(|S| < 5\),所以最大为 \(4\)

所以对于每个左端点,我们至多会枚举 \(4\) 次。
时间复杂度只有 \(O(n)\)

Third. 判三元组

蒟蒻太蒟了,看到这种偏序形式就用树状数组了。

先离散化,建 \(3\) 个树状数组,分别维护:权值 \(\le x\) 的点的数量,较大数权值 \(\le x\)\(a_i \ge a_j\) 的二元组数量,较小数权值 \(\le x\)\(a_i \le a_j\) 的二元组数量。

然后就判断点 \(i\) 能否接在之前的一个二元组上,能接上就是坏三元组。

其实暴力更快。

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int , int> pi;
#define endl '\n'
const int N = 2e5 + 10;
int n;
int a[N] , b[N];
struct bit {
#define lowbit(x) (x & -x)
    int c[N];
    void add(int x) {
        for (int i = x; i <= n; i += lowbit(i)) c[i]++;
    }
    void erase(int x) {
        for (int i = x; i <= n; i += lowbit(i)) c[i]--;
    }
    int sum(int x) {
        int res = 0;
        for (int i = x; i; i -= lowbit(i)) res += c[i];
        return res;
    }
};
bit t1 , t2 , t3;

signed main() {
    ios::sync_with_stdio(0) , cin.tie(0) , cout.tie(0);
    int t;
    cin >> t;
    while (t--) {
        cin >> n;
        for (int i = 1; i <= n; i++) cin >> a[i] , b[i] = a[i];
        sort(b + 1 , b + 1 + n);
        ll tmp = 0 , ans = 0;
        for (int i = 1; i <= n; i++) if (b[i] != b[i - 1]) b[++tmp] = b[i];
        for (int i = 1; i <= n; i++) a[i] = lower_bound(b + 1 , b + 1 + tmp , a[i]) - b;//离散化
        for (int i = 1; i <= n; i++) {
            bool flag = 1;
            for (int j = i; j <= n; j++) {
                if (t2.sum(a[j]) || t3.sum(n) - t3.sum(a[j] - 1)) {
                    flag = 0; // 不能暴力清空,会 TLE
                    for (int k = i; k < j; k++) {
                        t1.erase(a[k]);
                        if (t2.sum(a[k]) - t2.sum(a[k] - 1)) t2.erase(a[k]);
                        if (t3.sum(a[k]) - t3.sum(a[k] - 1)) t3.erase(a[k]);
                    }
                    break;
                }
                ans++;
                if (t1.sum(a[j])) t2.add(a[j]);
                if (t1.sum(n) - t1.sum(a[j] - 1)) t3.add(a[j]);
                t1.add(a[j]);
            }
            if (!flag) continue;
            for (int k = i; k <= n; k++) {
                t1.erase(a[k]);
                if (t2.sum(a[k]) - t2.sum(a[k] - 1)) t2.erase(a[k]);
                if (t3.sum(a[k]) - t3.sum(a[k] - 1)) t3.erase(a[k]);
            }
        }
        cout << ans << endl;
    }
    return 0;
}

参考资料

posted @ 2024-04-07 21:56  固态H2O  阅读(14)  评论(0编辑  收藏  举报