T1 P1069 [NOIP2009 普及组] 细胞分裂
这道题就是基本的数学知识。
我们直接来转化题意,这道题就是让我们求 min ( k i ) k i × m 1 m 2 ∣ S i k i \min (k_i) k_i\times m_1 ^ {m_2} \mid S_i^{k_i} min(ki)ki×m1m2∣Siki。
接下来就很好求了。
我们考虑先把 m 1 m_1 m1分解质因数,对于每一个 S i S_i Si,若 S i S_i Si中没有 m 1 m_1 m1中出现过的因子,那么对于这个 S i S_i Si,是没有相应的 k i k_i ki的。所以存在 k i k_i ki的充要条件是 m 1 m_1 m1中的质因数都在Si中出现过。那么对于 m 1 m_1 m1中的每一个质因子 p p p,将 m 1 m_1 m1中 p p p的指数 c c c除以 S i S_i Si中 p p p的指数 d d d并上取整(即 S i S_i Si的指数要是多少时才能满足可以整除的条件),取这些商的最大值就是所要花费的时间。那么对于所需时间取最小值即可。对于 m 1 m_1 m1是质数或1的情况要特判。
代码注释最详细的一集:
#include<bits/stdc++.h>
using namespace std;
const int N = 30005;
int p[N], c[N], cnt;//p存m1分解质因数后的质因数
//c用于存储对应质因数在m1中的指数
//cnt用于记录m1分解质因数后得到的不同质因数的个数
int main() {
int n;
scanf("%d", &n);
int m1, m2;
scanf("%d%d", &m1, &m2);
int m0 = m1;//分解质因数中逐步修改
//找S1中是否有在m1中出现的因子
//对m1进行质因数分解
bool pd = false; //判断m1是否为质数
for (int i = 2; i <= m1 / i && m0; i++) {
if (m0 % i == 0) {//找到了一个质因数,m1不是质数
cnt++;
pd = true;
p[cnt] = i;
}
while (m0 % i == 0) {
m0 /= i;
c[cnt]++;//p的指数
}
}
//若有答案,则m1中的质因数在Si中出现过
if (!pd) {
p[++cnt] = m1;
c[cnt] = 1;
}
int ans = 1e9;
while (n--) {
int s, res = -1e9;
//s 用于存储输入的数
bool flag = true;
scanf("%d", &s);
for (int i = 1; i <= cnt && s; i++) {
int jsq = 0;//jsq记录s中包含当前质因数p[i]的个数
if (s % p[i]) { //不满足上面所给出的充要条件
flag = false;
break;
}
if (p[i] == 1) { //特判m1是1的情况
puts("0");
return 0;
}
while (s % p[i] == 0) {
s /= p[i];
jsq++;
}
if (res < ceil(c[i]*m2 * 1.0 / jsq)) {
res = ceil(c[i] * m2 * 1.0 / jsq);
}
}
if (!flag) continue;
ans = min(ans, res);
}
if (ans == 1e9) puts("-1");
else printf("%d\n", ans);
return 0;
}
T2 P5629 【AFOI-19】区间与除法
这道题也挺好想的,我是使用了ST表。
首先我知道一个结论可以作为这道题的突破口:
一个数除以一个数等价于把原来的数在以除以的数为进制的表示下的末位舍弃。
下面来证明一下结论的正确性:
设我们有一个十进制数 N N N,要除以一个数 b ( b > 1 ) b(b>1) b(b>1),并且将 N N N转换为 b b b进制表示。
设 N = a n b n + a n − 1 b n − 1 + . . . + a 1 b + a 0 N=a_nb^n+a_{n-1}b^{n-1}+...+a_1b+a_0 N=anbn+an−1bn−1+...+a1b+a0,其中 0 ≤ a i < b , i = 0 , 1 , . . . , n 。 0 \le a_i<b,i=0,1,...,n。 0≤ai<b,i=0,1,...,n。
那么 N N N在 b b b进制下表示为 ( a n a n − 1 . . . a 1 a 0 ) b (a_na_{n-1}...a_1a_0)_b (anan−1...a1a0)b
当我们在计算 N ÷ b {N \div b} N÷b时:
N ÷ b = a n b n − 1 + a n − 1 b n − 2 + . . . + a 1 + a 0 b {N\div b}=a_nb^{n-1}+a_{n-1}b^{n-2}+...+a_1+\frac{a_0}{b} N÷b=anbn−1+an−1bn−2+...+a1+ba0
因为 a 0 a_0 a0是 N N N在 b b b进制下的末位数字, a 0 b \frac{a_0}{b} ba0是余数部分,商为 a n b n − 1 + a n − 1 b n − 2 + . . . + a 1 a_nb^{n-1}+a_{n-1}b^{n-2}+...+a_1 anbn−1+an−1bn−2+...+a1
在 b b b进制下, a n b n − 1 + a n − 1 b n − 2 + . . . + a 1 a_nb^{n-1}+a_{n-1}b^{n-2}+...+a_1 anbn−1+an−1bn−2+...+a1对应 b b b进制表示就是 ( a n a n − 1... a 1 ) b (a_na_{n-1...a_1})_b (anan−1...a1)b也就是舍弃了末位 a 0 a_0 a0
证毕
那么知道这个结论后这道题就很好解决了。
观察一下本题,发现每次操作都是连续除以一个数的,除以一个数等价于把原来的数在以除以的数为进制的表示下的末位舍弃,所以考虑转化把进制转化为除以的数。
可以得到一个贪心结论:只考虑一个数的最小的对应的原数即可,因为比它大的原数一定能在除以若干次后变成它。
核心思路完成了,就是怎么实现,观察数据范围,发现询问次数非常多,每次询问就必须复杂度很低,又发现 (m) 相当小,可以直接状态压缩,采用st表进行或运算即可,可以在预处理最小原数时用字典树来优化时间,不用也是可以过的。
注释超级详细的代码:
/*
观察一下本题,发现每次操作都是连续除以一个数的,
除以一个数等价于把原来的数在以除以的数为进制的表示下的末位舍弃,
所以考虑转化把进制转化为除以的数,
可以得到一个贪心结论:只考虑一个数的最小的对应的原数即可,
因为比它大的原数一定能在除以若干次后变成它。
状态压缩,
采用st表进行或运算即可,
可以在预处理最小原数时用字典树来优化时间,
*/
#include<bits/stdc++.h>
#define int long long
#define lg(x) log(x)
using namespace std;
const int N = 5e5 + 10;
int n, m;
int d, q;
int f[N][30], a[N], b[100];
//对f数组进行预处理,构建ST表
void st(int n) {
int t = lg(n) / lg(2) + 1;//2为底n的对数向上取整的值t,ST表中第二维的最大有效索引。
//逐步填充f数组,以便后续能够快速查询区间的合并结果。
for (int j = 1; j < t; ++j){
for (int i = 1; i <= n - (1 << j) + 1; ++i){
f[i][j] = f[i][j - 1] | f[i + (1 << (j - 1))][j - 1];
}
}
}
//计算整数x的最低位(即二进制表示下最右边的1所对应的数值)
int lb(int x) {
return x & (-x);
}
//用于查询区间[l, r]经过预处理后的某种状态结果
int get(int l, int r) {
//k指ST表中的层级
//通过已经预处理好的f数组在该层级上获取到覆盖区间[l, r]的子区间状态信息,并通过按位或运算合并得到v。
int k = lg(r - l + 1) / lg(2);
int v = f[l][k] | f[r - (1 << k) + 1][k];
int res = 0;
//利用lb函数找到v的最低位并减去,同时计数,直到v变为0
while (v) {
++res;
v -= lb(v);
}
return res;
}
signed main() {
scanf("%lld%lld%lld%lld", &n, &m, &d, &q);
for (int i = 1; i <= n; ++i){
scanf("%lld", &a[i]);
}
for (int i = 1; i <= m; ++i){
scanf("%lld", &b[i]);
}
sort(b + 1, b + 1 + m);
for (int i = 1; i <= n; ++i) {
int x = a[i], v = -1;//v=-1表示尚未找到对应的有效索引。
int p = lower_bound(b + 1, b + 1 + m, x) - b;
if (p <= m && b[p] == x)v = p;
x /= d;
while (x) {
int p = lower_bound(b + 1, b + 1 + m, x) - b;
if (p <= m && b[p] == x)v = p;
x /= d;
}
//如果v不为 -1,说明找到了对应的有效索引,即将第v位设置为 1
if (v != -1)f[i][0] = 1ll << v;
}
st(n);
while (q--) {
int l, r;
scanf("%lld%lld", &l, &r);
printf("%lld\n", get(l, r));
}
return 0;
}
T3 P1528 切蛋糕
这道题暴力思路还是很好想出来的:
直接深搜寻找最优决策就行了。
57 p t s 57pts 57pts注释超详细的代码:
//深搜暴力
//寻找最优决策
#include<bits/stdc++.h>
using namespace std;
const int N=1500;
int n, m;
int ans;//记录答案
int cake[N],mouth[N];//蛋糕大小and嘴的大小
//尝试所有可能的蛋糕分配方案;
//以找到能够满足最多嘴的蛋糕分配方式;
//从而最大化ans的值。
void dfs(int x, int y) {//y:已经成功分配了蛋糕的嘴的数量
//x:考虑分配到了第几张嘴
if (x == m + 1) {//已经找完
ans = max(ans, y);
return;
}
dfs(x + 1, y);//不选择任何蛋糕来满足当前第x张嘴,直接考虑下一张嘴的分配情况
for (int i = 1; i <= n; i++) {
if (cake[i] >= mouth[x]) {
cake[i] -= mouth[x];//剩下的蛋糕
dfs(x + 1, y + 1);//已经成功用一个蛋糕满足了当前嘴
cake[i] += mouth[x];//将蛋糕的值加回去,重新找
}
}
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> cake[i];
}
cin >> m;
for (int i = 1; i <= m; i++) {
cin >> mouth[i];
}
dfs(1, 0);
cout << ans;
return 0;
}
下面来想想正解:
一道很朴素的搜索题。
下面来根据生活常识思考一下:
①一块蛋糕一定能满足更多的嘴小的人,而不是更多嘴大的人。所以,贪心的想:如果将人按照嘴的大小排序,是不是更好呢?
②题目中明确规定不能将两块蛋糕拼起来,所以,如果一个人嘴大到连最大的蛋糕都无法满足,那就可以把他踢出去了,那么就不再考虑这个人了,因为无论如何都满足不了。所以依次(从大到小)把嘴大于最大的蛋糕的人全部排除掉,减少搜索判断次数,就可以起到优化的效果了。
③我们要先假设能满足的人数,显然多一次假设就会多一次搜索,那么减少假设次数便是一个关键。于是我们便想到了二分,如果使用二分的话,就可以大大减少假设次数。
④剪枝优化1:每一次试探中蛋糕都会有所浪费,要判别是否浪费。显然,如果一块蛋糕分给一个人一部分后,剩下的连嘴最小的人都满足不了了,那么就一定浪费了。如果蛋糕总量减去浪费的蛋糕量(即这种方法剩下的蛋糕总量)比前n个人的总需求量还小,那么就说明无论如何都满足不了这n个人了,应该立即返回0,表示这种方案失败,换一种方案再继续搜索,这样就会少递归很多次。
⑤剪枝优化2:如果前一个人和这个人的嘴一样大,那么就可以优化判断前面的人是否可以被满足,减少for循环次数,由于这个人在第i个蛋糕之前的蛋糕都已经无法满足了(所以才会使用第i个蛋糕来满足),因为前一个人和这个人嘴一样大,所以前i个蛋糕也无法满足这个人了,因此,下一次循环可以特意从当前的i开始循环,而 s a m e s t a r t same_{start} samestart也就是这样起到标记效果的。
注释详细的代码:
#include<bits/stdc++.h>
using namespace std;
int mouth[10000], cake[10000], c[10000], pre[10000], waste, all, n, m, l, r, mid, ans, maxn; //变量的意思(依次)
//嘴的大小、蛋糕的大小、蛋糕大小副本、前缀和、浪费值、蛋糕总体积 、蛋糕个数、嘴的个数、二分左边界、右边界
//二分的中点、答案、最大的蛋糕的体积
int dfs(int num, int same_start) //num表示剩下的要满足的人数,同时也巧妙的表示了人嘴的编号
//same_start未剪枝时都可以当做1,而在进行剪枝时的用处详见上文
{
if (num == 0) return 1; //能够分配完所有的人,那么返回1
if (all - waste < pre[mid]) return 0; //剪枝优化1,详见上文
for (int i = same_start; i <= n; i++) {
if (c[i] >= mouth[num]) {
c[i] -= mouth[num]; //如果能满足这个人,那么就让他吃掉,进行试探
if (c[i] < mouth[1]) waste += c[i]; //如果连口最小的人都满足不了,
//那么只能浪费,于是增加浪费值,便于上面的剪枝
if (mouth[num] == mouth[num - 1]) { //剪枝优化2,详见上文
if (dfs(num - 1, i)) return 1;
} else if (dfs(num - 1, 1)) return 1;
if (c[i] < mouth[1]) waste -= c[i]; //回溯
c[i] += mouth[num]; //回溯
}
}
return 0;//如果是可以满足的,那么上面会返回的,否则就是不可以满足,返回0
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> cake[i];
all += cake[i]; //记录蛋糕总量,用于剪枝
if (cake[i] > maxn) maxn = cake[i]; //记录最大的蛋糕大小,后面有用
}
cin >> m;
for (int i = 1; i <= m; i++)
cin >> mouth[i];
sort(mouth + 1, mouth + m + 1); //贪心:将每个人按嘴的大小排序
while (maxn < mouth[m]) m--; //从嘴最大的人开始,如果嘴比最大的蛋糕还大,那么一定无法满足
//因为无法将两块蛋糕拼起来给人
for (int i = 1; i <= m; i++)
pre[i] = pre[i - 1] + mouth[i]; //记录前缀和,用于剪枝
l = 0;
r = m; //规定好二分查找左右边界
while (l <= r) {
waste = 0; //浪费值初始为0
mid = (l + r) / 2;
for (int i = 1; i <= n; i++) c[i] = cake[i]; //如果在搜索中用cake数组的话,可能在没有回溯前就返回了,
//那样cake值会变,影响下一轮搜索,所以赋值到c数组中,使用c数组代替,就像是常说的副本一样
if (dfs(mid, 1)) {
ans = mid; //如果这个猜测能完成,那么就要记录下答案,不停覆盖,直到最后找到
l = mid + 1;
} else r = mid - 1; //注意:这里千万不要写成r=mid,
//因为当l=r时,mid=l=r,如果r=mid,那么就会陷入死循环,可以自己模拟一下,l会永远等于r
//这种情况不可能r<l,所以会一直循环
//或者这里写成r=mid,但是while的小括号里必须换一个判定条件:l<r也是可以的
}
cout << ans;
return 0;
}
T4 P3750 [六省联考 2017] 分手是祝愿
哎,分手了。竟然还是祝愿!!!还不是住院!!!
这道题是期望dp+质因数分解。
先从这个题跳出来,思考如何用最少的次数将灯关掉。
因为一个灯能控制的灯都小于他,所以最右边的灯一定要按,因为如果按他的倍数就更不优,所以就从高到底扫一遍,遇到亮的灯就点一次,因为每个灯都是不可替代的,所以这启示我们这些灯在方案中是必须要按的。
求出这些灯的个数
c
n
t
cnt
cnt,也就是最少要按的次数,如果
c
n
t
cnt
cnt小于等于
k
k
k,则直接输出
c
n
t
cnt
cnt即可
所以设
f
[
i
]
f[i]
f[i]表示将剩下
i
i
i盏必须要按的灯变成
(
i
−
1
)
(i-1)
(i−1)盏必须要按的灯的期望次数
f [ i ] = i n × 1 + n − i n × ( f [ i ] + f [ i + 1 ] + 1 ) f[i]=\frac{i}{n} \times1+\frac{n-i}{n} \times(f[i]+f[i+1]+1) f[i]=ni×1+nn−i×(f[i]+f[i+1]+1)
该式子的含义为有 i n \frac{i}{n} ni的概率按对,也就是将 i i i盏必须要按的灯变为 i − 1 i-1 i−1盏需要按的灯,有 n − i n \frac{n-i}{n} nn−i的概率按错,回到 i + 1 i+1 i+1盏灯的情况,然后需要再按 f [ i ] f[i] f[i]次按回来,最后的 1 1 1是这一次的贡献。
将式子化简得:
f [ i ] = 1 i × ( n + ( n − i ) × f [ i + 1 ] ) f[i]=\frac{1}{i} \times(n+(n-i)\times f[i+1]) f[i]=i1×(n+(n−i)×f[i+1])
最后答案就是 f [ c n t ] + f [ c n t − 1 ] + . . . + f [ k + 1 ] + k f[cnt]+f[cnt-1]+...+f[k+1]+k f[cnt]+f[cnt−1]+...+f[k+1]+k。
也就是将 c n t cnt cnt盏灯变为 c n t − 1 cnt-1 cnt−1盏灯,一直变到 k k k盏灯最后按 k k k次按完。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5 + 7;
const int mod = 1e5 + 3;
int n, k, cnt = 0;
int a[N];
vector<int> fac[N];
LL inv[N], F[N], ans;
LL Pow(LL a, int b) {
LL res = 1;
while (b) {
if (b & 1) res = res * a % mod;
a = a * a % mod;
b >>= 1;
}
return res % mod;
}
int main() {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
for (int i = 1; i <= n; i++) {
for (int j = i; j <= n; j += i)
fac[j].push_back(i);
}
for (int i = n; i >= 1; i--) {
if (a[i] == 1) {
for (int p = 0; p < fac[i].size(); p++) {
int j = fac[i][p];
a[j] ^= 1;
}
cnt++;
}
}
if (cnt <= k) ans = cnt % mod;
else {
for (int i = 1; i <= n; i++)
inv[i] = Pow(i, mod - 2) % mod;
F[n] = 1;
for (int i = n - 1; i >= 1; i--)
F[i] = (i + (n - i) * (1 + F[i + 1])) * inv[i] % mod;
for (int i = cnt; i > k; i--)
ans = (ans + F[i]) % mod;
ans = (ans + k) % mod;
}
for (int i = 1; i <= n; i++)
ans = (ans * i) % mod;
cout << ans;
return 0;
}
分手应该听体面
总结
这次的题挺好的,T2挺考验思维难度,得推结论,dp的题出的有点难了,暴力分没打,T3暴力有分,T1挂了一个点,一些不常用的也都考了。