CodeCraft-21 and Codeforces Round #711 (Div. 2) A~E题解
题目链接
这一场打的很差,B题卡了四十分钟,然后C题因为没有对负数取模,wa了,又白给掉分。
A题
题意:给你一个数n,让你找到一个数\(ans >= n\),设ans的所有数位和为sum, 使得\(gcd(ans, sum) > 1\)
思路:只要ans是3的倍数,就一定满足\(gcd(ans, sum) >= 3\), 因此直接暴力,复杂度也不高
代码如下
ll gcd(ll a, ll b)
{
return b ? gcd(b, a % b) : a;
}
bool check(ll x) ///检查是否满足题目要求
{
ll sum = 0;
ll d = x;
while(d) sum += d % 10, d /= 10;
if(gcd(x, sum) > 1) return true;
return false;
}
int main()
{
while(T --)
{
ll x;
cin >> x;
while(!check(x)) x ++;
cout << x << endl;
}
}
B题
题意:有一个宽度是w的盒子,有n个宽度都是2的幂次的方块,问把方块全放进盒子里至少要放多少层
思路:由于方块宽度都是2的幂次,因此直接把所有方块排序后,每一层都不断取最大能放进盒子里的方块,直到取不了为止,一定是最佳的
把所有方块宽度映射到0~19,因为\(2^{20} > 1e6\), 并统计数目, 然后进行上述操作,能有效提高运行效率
代码如下
int a[N];
map<int, int> mp;
int cnt[30];
int base[30];
bool check() //检查是否所有方块都已经用完
{
for(int i = 0 ; i <= 20 ; i ++)
if(cnt[i])
return true;
return false;
}
int main()
{
for(int i = 0 ; i < 30 ; i ++)
mp[1 << i] = i, base[i] = 1 << i;
while(T --)
{
cin >> n >> w;
for(int i = 1 ; i <= n ; i ++)
{
cin >> a[i];
cnt[mp[a[i]]] ++;
}
int res = 0;
while(check()) //只要方块还没有用完,就每次都新开一层
{
ll sum = 0;
for(int i = 20 ; i >= 0 ; i --) //然后从大往小往箱子里塞方块,直到不能塞为止
{
while(cnt[i] && sum + base[i] <= w)
sum += base[i], cnt[i] --;
}
res ++; //记录层数
}
cout << res << endl;
}
}
C题
题意:有n块平行的墙体,给定一个可以穿越墙体的微观粒子寿命为k,但是每穿越一次墙体会产生一个反向寿命为k - 1的粒子。
请问最终共有多少个粒子?
思路:对寿命为k-1~1的粒子进行模拟,每次寿命为x的粒子在相邻的两个墙体间由寿命为x+1的粒子产生的数目都可以计算出来, 因此从k转移到k-1开始不断转移,一直到1,就能计算出总的粒子数。
可以自己在纸上画一下,多转移几次就能发现转移规律。
特判一下不需要转移的情况,可以避免处理一些边界。注意减法也要取模。。。
当然,能想到下面的大佬DP是最好的,精简凝练,但比赛的时候,能AC就行啦!
代码如下
ll n, k;
ll s[N];
ll a[N];
int main()
{
while(T --)
{
cin >> n >> k;
ll res = 0;
if(k == 1) cout << 1 << endl;
else if(n == 1) cout << 2 << endl;
else
{ //只关心在墙壁间运动的粒子,因此第一个发射的粒子和第一次产生的不进入墙壁粒子,总共有2个
res = 2;
for(int i = 1 ; i < n ; i ++)
a[i] = 1, s[i] = s[i - 1] + a[i]; //第一个粒子在墙壁间产生的粒子
ll d = k;
d --; //第一次已经预处理了,现在粒子还剩下k-1的寿命
while(d --) //模拟每个寿命的粒子情况
{ //首先加上产生当前粒子的粒子(即寿命为k+1的且在墙壁间的粒子) 总数就是前缀和
res = (res + s[n - 1]) % mod;
if((d & 1) != (k & 1)) //考虑方向 (下面的递推公式就自己在纸上画出来发现的)
{
for(int i = 1 ; i < n ; i ++)
a[i] = s[i];
for(int i = 1 ; i < n ; i ++)
s[i] = (s[i - 1] + a[i]) % mod;
}
else
{
for(int i = 1 ; i < n ; i ++)
a[i] = ((s[n - 1] - s[i - 1]) % mod + mod) % mod;
for(int i = 1 ; i < n ; i ++)
s[i] = (s[i - 1] + a[i]) % mod;
}
}
cout << res << endl;
}
}
}
大佬DP代码
ll n, k;
ll f[N][N]; //f[i][j]表示寿命为i 还要穿过j次墙的粒子数
int main()
{
while(T --)
{
cin >> n >> k;
for(int i = 1 ; i <= k ; i ++) f[i][0] = 1;
for(int i = 1 ; i <= k ; i ++)
for(int j = 1 ; j <= n ; j ++)
f[i][j] = (f[i][j - 1] + f[i - 1][n - j]) % mod;
//f[i][j - 1]表示当前穿过了一次墙,还有 j - 1 次墙要穿过
//f[i - 1][j - 1] 表示穿过墙 产生了要穿过 n - j 次墙的寿命为 i - 1的反向粒子
cout << f[k][n] << endl;
}
}
D题
题意:给你n个时间,每个时间可以进行两种操作中的一种, 由输入决定可以进行哪一种,其中一种是\(\lceil k + x \rceil\), 另一种是 \(\lceil k * x \rceil\), 可以进行0~y次操作, k初始为0,问k变成1~m中每个数的最早时间是多少?
思路:很容易想到直接暴力的做法,首先从1 ~ n枚举时间,然后每次枚举当前已经出现过的数,然后在当前时间片对每一个数都进行1~y次操作,新出现的数一定是最早出现的,直接记录下答案。但是这种做法的时间复杂度是\(O(NM^2)\)的,这是肯定会TLE的,因此我们要看看有没有办法优化。显然,在我们用之前已经出现过的数进行计算当前时间出现的数时,如果计算出来的数已经出现过,那么我们一 定可以停止(break)继续用后面的数计算,因为计算后面出现的数用当前遇到的这个已经出现过的数一定是更优的。举个例子,已经出现的数是\([3, 7]\), 现在操作是加,\(x = 4, y = 5\), 那么显然,我们用3计算能够得到\([3, 7, 11, 15, 19, 23]\), 用7计算能得到\([7, 11, 15, 19, 23, 27]\), 明显出现了大量重复计算,如果采取上述策略,在用3计算出7时,7已经上次就出现了,直接break,换一个数7来计算,那么每个数只会被计算很少的次数,是\(O(NM)\)的,可以接受。
代码如下
int n, m;
struct node{
int type;
ll x;
int y;
}p[N];
int ans[M];
vector<ll> v;
int main()
{
IOS;
cin >> n >> m;
for(int i = 1 ; i <= n ; i ++)
cin >> p[i].type >> p[i].x >> p[i].y;
ll k = 0;
v.push_back(0);
for(int i = 1 ; i <= n ; i ++) //从前往后枚举时间
{
ll c = p[i].x;
int len = v.size();
if(p[i].type == 1) //看是哪一种操作
{
for(int j = 0 ; j < len ; j ++) //遍历之前已经出现过的数
{
int t = p[i].y;
ll d = v[j];
while(t --) //用已经出现过的数进行1~y次操作
{
d = d + (c + 100000 - 1) / 100000;
if(d > m || ans[d]) break; //如果已经出现过或者越界直接break
ans[d] = i;
v.push_back(d);
}
}
}
else //乘法和加法同理
{
for(int j = 0 ; j < len ; j ++)
{
int t = p[i].y;
ll d = v[j];
while(t --)
{
d = (d * c + 100000 - 1) / 100000;
if(d > m || ans[d]) break;
ans[d] = i;
v.push_back(d);
}
}
}
}
//输出答案
for(int i = 1 ; i <= m ; i ++)
if(!ans[i]) cout << -1 << " ";
else cout << ans[i] << " ";
cout << endl;
return 0;
}
E题
题意:这是一个愉快的交互题,给你一个有n个点的有向图,任意两点间都有一条边,现在给出每个点的入度\(k_i\),你可以对任意的\(i, j,i \neq j\)查询是否有\(i\)到\(j\)的路径,但是当查询结果为Yes时,你必须给出答案。现在问你是否能找到一对点\(i, j\),使得点\(i\)和\(j\)能够相互到达并且\(\mid k_i - k_j\mid\)的值最大?
思路:现在假设我们已经对这个图求了强联通分量,那么我们缩点后可以得到一个拓扑图(极端情况是所有点都能相互到达,最后缩点成一个点),假设现在有两个强联通分量A和B,且边是由A指向B,那么此时A中点的最大可能入度也只能是A.size - 1,而B中点的最小入度也只能是A.size + 1,因此此时A中所有点的入度都小于B中的任意点。注意到任意两个点间都有一条边,那么最后得到的拓扑图只能是一条链状的,不可能出现分叉(因为出现分叉就能找到两个分叉上的点\(i, j\), \(i\)不能到\(j\), \(j\)也不能到\(i\), 矛盾)。综上,对任意的两个点,只要\(k_i < k_j\), 那么就一定存在一条i到j的路径。因此我们在查询时,只需要对\(k_i >= k_j\)的点进行查询,只要系统回复为"Yes", 这就是一个可行解。那么如何求最大的\(\mid k_i - k_j\mid\)呢?很简单,只需要预处理出需要查询的点对,然后按照\(\mid k_i - k_j\mid\)从大到小排序,然后依次查询,第一个"Yes"就是答案。
代码如下
int n;
int a[N];
struct node{
int a, b, c;
bool operator < (const node &w) const{
return c > w.c;
}
};
vector<node> v;
int main()
{
IOS;
cin >> n;
for(int i = 1 ; i <= n ; i ++) cin >> a[i];
for(int i = 1 ; i <= n ; i ++) //预处理出要查询的点对
for(int j = 1 ; j <= n ; j ++)
{
if(i == j) continue;
if(a[i] >= a[j])
v.push_back({i, j, a[i] - a[j]});
}
string s;
sort(v.begin(), v.end()); //排序
for(int i = 0 ; i < v.size() ; i ++) //依次查询
{
auto t = v[i];
cout << "? " << t.a << " " << t.b << endl;
cout.flush();
cin >> s;
if(s == "Yes")
{
cout << "! " << t.a << " " << t.b << endl;
cout.flush();
return 0;
}
}
cout << "! 0 0" << endl;
cout.flush();
return 0;
}