【思维专练】专题训练 1

前言

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

  • CF1681D Required Length

TAGS:暴搜 + 剪枝

前置函数

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

First. 为什么是搜索

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

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

所以,贪心无法做。

再看一眼数据范围,发现 n19,那么极端一点,对于 2
每次都只 ×2,最多也只需要 log21019 次。

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

Second.如何剪枝

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

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

  3. 最优化剪枝2:如果当前到达 x 的步数 之前到达 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

TAG: 构造

First. 2 的位置

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

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

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

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

xn1 为,n 的操作范围,那么操作次数即为:nx+nlogx1

x=n+1 时,该式取得最小值。

所以,对于 n+1n 的这一段区间需要的次数即为:nn+2,比区间长度多 1

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

而每次操作,量级缩小至 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

TAG:枚举

First. 缩点

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

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

Second. 定位

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

对于后 i*,他们可以生成的字符串的种类数为 faci=j=incntj

找到第一个 <xfaci,记为 pos

而对于 pos1,它每增加一个 b,就会增加 facpos 种字符串,所以 pos1 需要展开的个数,即为 xfacpos

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

Tips

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

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

TAG:暴力、BIT、组合数学

一开始震惊到我了,O(n2log2n) 过了,然后一算发现是 O(nlog2n) 的。

First. 解析坏的三元组

画个图发现,当且仅当 x1x2x3y1y2y3y1y2y3 的时候构成坏三元组。

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

Second. 没有坏三元组的区间

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

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

证明

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

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

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

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

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

Third. 判三元组

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

先离散化,建 3 个树状数组,分别维护:权值 x 的点的数量,较大数权值 xaiaj 的二元组数量,较小数权值 xaiaj 的二元组数量。

然后就判断点 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 @   固态H2O  阅读(22)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示