目录
所有题目链接:Dashboard - 2020 ICPC Shanghai Site - Codeforces
铜牌题:G, B, M, D
银牌题:H, E, I, L, C
金牌题:K, F, A, J
F,A,J三题尚未完成
G题 数学,签到
题意:
为斐波那契数列,求,其中
分析:
注意到斐波那契数列中第项和第项是奇数,项是偶数。
则斐波那契数列前n项中,有个奇数
所以答案为
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
signed main() {
int n; cin >> n;
int m = (n / 3);
m = m * 2 + n % 3;
cout << n * (n - 1) / 2 - m * (m - 1) / 2;
}
B题 构造,思维题,基础
题意:
给出扫雷地图A和B,一个地图的权值定义为所有非雷块的权值之和,每个非雷块的权值定义为以它为中心的九宫格的雷块个数之和。要求修改B中不超过个块,使得A和B的权值一样多,并输出修改后的B
分析:
本题关键在于注意到A和A的相反图的权值总和相同,其中相反图定义为A中所有的雷块变为非雷块,非雷块变为雷块。
证明:图的权值实质上是如下“方块对”的数量——即两个方块在同一个九宫格内,而一个为雷块一个为非雷块。所以雷块和非雷块翻转后不影响总的权值和。
比较A和B不同的点的数量,记为。若,直接输出A。否则,输出A的相反矩阵
代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
char a[1005][1005], b[1005][1005];
signed main() {
int n, m;
cin >> n >> m;
int cnt=0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> a[i][j];
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> b[i][j];
if (a[i][j] != b[i][j])
cnt++;
}
}
if (cnt <= n * m / 2) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cout << a[i][j];
}
cout << '\n';
}
}
else {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i][j] == 'X')cout << '.';
else cout << 'X';
}
cout << endl;
}
}
return 0;
}
M题 模拟,树,基础
题意:
有一个树形文件系统。输入n+m行文件路径,格式为:slash1/slash2/.../file,slash为文件夹,file为文件,保证输入的路径一定是以某个文件结尾(叶子结点)。
可以选择某些文件或者文件夹删掉。如果删掉一个文件夹,会把它包含的文件夹和文件全部删掉。
前n行路径表示的文件需要被删掉,后m行路径中的文件不能被删掉。
求删掉的文件或文件夹的最少数量。
分析:
给出的一个文件路径就是树上从根节点到叶子结点的一条路径。
m个不能删掉的节连同它们的祖先构成了一个子图G。把G上的所有节点打上标记flag。
对于每一个需要删掉的点,从上向下遍历它第一个没有flag标记的祖先(直接遍历它的文件路径即可),并将它删掉。可以把已经删掉的点也打一个删除标记vis,避免重复删除。
特别的,本题只需要保存每个节点的路径,而不需要特别建树。因为本题只需要从下向上搜索祖先,而每个路径都包含着所有祖先节点的信息。
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
map<string, int> flag, vis;
int n, m;
signed main() {
int t; cin >> t;
while (t--) {
string node[200];
flag.clear();//用来标记某个节点是否被保留
vis.clear();//用来标记某个节点是否已经被删掉
cin >> n >> m;
//储存需要删除的所有叶子结点
for (int i = 1; i <= n; i++) {
cin >> node[i];
}
//将所有需要保留的节点打上标记flag
for (int i = 1; i <= m; i++) {
string s;
cin >> s;
for (int j = 0; j < s.size(); j++) {
if (s[j] == '/') {
flag[s.substr(0, j)] = 1;
}
}
}
//对于每个需要删掉的叶子节点,从根向下找到它第一个没有打上标记的祖先节点
//无论该节点是否已被删掉,都打上删掉的标记,并退出
int ans = 0;
for (int i = 1; i <= n; i++) {
int tag = 1;
for (int j = 0; j <= node[i].size(); j++) {
if (node[i][j] == '/') {
string ss = node[i].substr(0, j);//所有结束在'/'之前的前缀都是node[i]的祖先节点
if (flag[ss]) tag = 1;//该祖先需要被保留
else {//找到第一个不需要保留的祖先
if (vis[ss]) tag = 0;//如果该祖先节点已经被删掉,那这次就不需要删了
vis[ss] = 1;
break;
}
}
}
if (tag) ans++;
}
cout << ans << endl;
}
return 0;
}
D题 二分答案,基础
题意:
给一个长度为n的线段。两个人给定初始位置和速度, ,方向可随时随意改变。问最短多久两个人能遍历整个线段。
分析:
设左边的人位置为,右边的人位置为,两个人将线段可分为三部分,其中;
二分答案,对于时间t求出两个人能走的距离 和
若其中一个人就能走完全程,则直接返回true;若某个人到达距离自己较近的点,那么另一个人必须走完全程,而该情况已经排除,所以直接返回false;
于是剩下的情况中,两个人都有能力到达距离自己较近的边界,但都不能独自走完全程。
此时求左边的人能到达的最右端,和右边的人能到达的最左端。以左边的人为例,可能先向左到达端点再向右超过出发点(),或先向左到达端点但是不足以回到出发点(),或先向右到达某个距离再向左到达端点(),对取最大值即可, 同理。若则可认为能够相遇(覆盖全程)
代码(copy lhf):
#include <bits/stdc++.h>
#include <array>
using namespace std;
#define int long long
typedef unsigned long long ull;
typedef unsigned int uint;
int t = 1;
const int mod = 998244353;
const int inf = 1e12;
const int N = 2e5 + 5, M = 2e5 + 5;
const long double eps = 1e-10;
long double n, p1, p2, v1, v2;
bool check(long double t) {
long double s1 = v1 * t, s2 = v2 * t;
if (s1 >= n + min(p1, n - p1) || s2 >= n + min(p2, n - p2))
return true; //某个人独自走完全程
if (s1 < p1 || s2 < n - p2)
return false;//有一个人不能到达离自己较近的边界
long double l, r;
r = max(s1 - p1, 0.5 * (s1 + p1));
l = min(n - (s2 - (n - p2)), n - (0.5 * (s2 + n - p2)));
r = max(r, p1);
l = min(l, p2);
if (r - l >= eps)return true;
else return false;
}
void init() {
}
void solve(){
cin >> n >> p1 >> v1 >> p2 >> v2;
if (p1 > p2)swap(p1, p2), swap(v1, v2);
long double L = 0.0, R = max((n - p1) / v1, p2 / v2);
while (R - L >= eps) {
long double mid = 0.5 * (L + R);
if (check(mid))R = mid;
else L = mid;
}
cout << fixed << setprecision(10) << 0.5 * (L + R) << "\n";
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> t;
while (t--) {
init();
solve();
}
return 0;
}
H题 构造,中档
题意:
有一个圆形的桌子,共有n个点。有k个客人坐在桌旁,k盘菜放在桌上,位置已知。可以顺时针或逆时针转动桌子,使得所有的菜一起转动。为了使每个客人都有一盘菜,求转动的最小距离
分析:
先把人和菜排序,方便处理。
最终每个人对应每一道菜。如果人i在j的顺时针方向,则i的菜也一定在j的顺时针方向(贪心即可证明),也就是说所有人和菜的连线不会有交点。因此只要第一个人对应了某道菜,则所有的人对应的菜就确定了。
于是枚举第一个人对应的菜,共k种情况。
对于每一种情况可先预处理每个人要想拿到自己的菜需要顺时针转动的距离。
可以贪心证明转动方向的改变次数不会多余1次:若先向右转,再向左转,再向右转,则第一次向右转一定是多余的。
于是只有四种方式满足所有人:一直顺时针转下去直到每个人拿到自己的菜;一直顺时针转下去直到每个人拿到自己的菜;先顺时针转到某个人i拿到自己的菜,再逆时针转让其他人拿到菜;先逆时针转到某个人i拿到菜,再逆时针转让其他人拿到菜。枚举i即可。
代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn = 1e5 ;
ll a[maxn], b[maxn], dis[maxn];
int main() {
int t;cin>>t;
while (t--) {
ll n, k;cin>>n>>k;
for(int i=0;i<=k-1;i++) cin>>a[i];
for(int i=0;i<=k-1;i++) cin>>b[i];
sort(a, a + k);
sort(b, b + k);
ll ans = n*100;
for (int rot = 0; rot <= k - 1;rot++) {
for(int i=0;i<=k-1;i++) dis[i] = (b[(i + rot) % k] - a[i] + n) % n;
sort(dis, dis + k);
ans = min({ ans, dis[k - 1], n - dis[0] });
for (int i = 0; i <= k - 2;i++) {
ans = min(ans, 2 * dis[i] + (n - dis[i + 1]));
ans = min(ans, 2 * (n - dis[i + 1]) + dis[i]);
}
}
cout<<ans<<'\n';
}
}
E题 dp优化,组合,中档
题意:
给定1<k<n,满足以下性质的1~n的排列a称为“好排列”:
求好排列的个数
分析:
对于一个包含m个数字的“好序列”,设其中最小的数字是 ;
若,则包含个数字的好序列有 种;
若只要 ,且后个数字组成的序列是“好序列”,那么原序列是好序列。证明:思路是对于,证明 满足定义。由于后个数字的序列是好序列,则对于,一定满足原定义要求;而对于,有,而一定成立,因此也满足原定义要求。综上证毕。
设表示长度为的序列的所有排列中,好序列的个数。由上述性质,只需要枚举最小元素的位置,安排好前个元素,再安排后个元素组成的好序列。
则,但是暴力转移复杂度O(nm);
继续化简: ;
维护
则
用就可以实现所有转移。
注意先预处理逆元
代码(参考网络):
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn = 1e7 + 5;
const ll mod = 998244353;
ll dp[maxn], pre[maxn], fac[maxn], inv[maxn];
ll powmod(ll a, ll b) {//快速幂
ll ans = 1;
while (b) {
if (b & 1) ans = ans * a % mod;
a = a * a % mod;
b >>= 1;
}
return ans;
}
void init() {//处理阶乘,
fac[1] = fac[0] = 1;
for (int i = 2; i < maxn; ++i) fac[i] = fac[i - 1] * i % mod;
inv[maxn - 1] = powmod(fac[maxn - 1], mod - 2);
for (int i = maxn - 2; i >= 0; --i)
inv[i] = inv[i + 1] * (i + 1) % mod;
}
signed main() {
ll n, k;
cin >> n >> k;
init();
dp[1] = 1, pre[1] = 2, pre[0] = 1;
for (int m = 2; m <= n; ++m) {
if (m <= k)
dp[m] = fac[m-1] * pre[m - 1] % mod;
else
dp[m] = fac[m-1] * (pre[m - 1] - pre[m - k - 1] + mod) % mod;
pre[m] = pre[m - 1] + dp[m] * inv[m] % mod;
if (pre[m] > mod) pre[m] -= mod;
}
cout << dp[n] << endl;
}
I题 几何,组合计数,中档
题意:
有n个圆,全部以(0,0)为圆心,第i个圆半径为i。有m条直线,每条直线都经过原点,这m条直线把每个圆都分成均匀的2m份。求图中所有的点对(a,b)的距离和。
分析:
若两个点在同一个圆上,则它们之间的距离为直径和劣弧的较小者;若两个点不在一个圆上,则外层点向内走到内层点的圆上,再加上同圆上两点距离;
令表示 第 个圆上某点,到 其内部 个圆 以及 第 个圆上 所有点 的距离和;
令表示 第 个圆上某点,到 这个圆上所有点 的距离和;
由相似,;
由两部分组成:第i个圆上的点之间的距离和 ,以及第i个圆到其内部的圆上的节点的距离和。至于第二部分,是第 个圆上的点先往里走一步到第个圆上,然后再加上 ,由于内部有 个点,所以要往里走 次。综上,
求和的时候从内向外枚举每个圆,枚举到第 个圆就在 里加上第 个圆上的点到内部个圆上的点的距离和,再加上第 个圆上所有不同两点的距离之和
注意,如果,则直线在圆心处也有交点。此时需要加上圆上所有点到圆心的距离
代码:
#include<bits/stdc++.h>
using namespace std;
const int mx = 5001;
const double pi = acos(-1);
double a[mx], b[mx];
int main() {
int n, m; cin >> n >> m;
double cnt = 0;
for (int i = 1; i < m; i++) {
if (pi * i < 2 * m) {
cnt += pi * i / m;
}
else {
cnt += 2;
}
}
cnt *= 2; cnt += 2;//现在的cnt是第一个圆上的某一点到该圆所有点的距离和
//a[i],b[i]意义如上分析
a[1] = b[1] = cnt;
for (int i = 2; i <= n; i++) {
b[i] = b[1] * i;
a[i] = b[i] + a[i - 1] + (i - 1) * 2 * m;
}
double ans = 0;
for (int i = 1; i <= n; i++) {
ans += 2 * m * (a[i] - b[i]) + m * b[i];
//如果m大于1说明原点位置有交点,要求出每个圆上的点到原点的距离和。
if (m > 1)
ans += 2 * m * i ;
}
printf("%.10lf\n", ans);
return 0;
}
L题 构造,几何,中档
题意:
有一个 的网格图,现在要从(0,0)点走到(n,m)点,满足每次只能走到一个整点,路径为两点连成的直线段,这条线段上不能有其他整点,两点之间的路程即这条线段的长度(欧几里得距离),且不能连续两次往同一方向走,问最短的路程。
()
分析:
若,则直接一步到达;
否则,设直线 为从起点到终点的对角线,设dis为两点间的欧几里得距离;从1~m枚举横坐标x,求最大的y满足点(x,y)在 下方,则;
下面证明:为什么一定是两条折线而不是更多,为什么是最大的y,是否会经过整点
首先,路线最多由两条路线组成。如下图,只要是三条折线,一定会被“三角形任意两边大于第三边”被优化掉,如下图的长度一定大于 和 。至于是否会经过整点归到后面讨论。
(我自己画的图,画的比例有点夸张,而且是个梯形)
其次,一定要选在直线 下的最上方的点,如图, 。原因是 , 则相加不等号不变。
最后,如果某条折线上面有整点,那么这种情况一定不是最优情况,例如枚举x得到点P后,发现PT上面有一个整点,那么OP+PT一定不是最优解,因为显然OQ+QT>OP+PT。因此只要折线上有整点,那么另一整点一定会将这个折线优化掉。因此最小的折线上一定没有整点。
代码:
#include <bits/stdc++.h>
using namespace std;
#define dis(x, y) sqrt(1.0 * (x) * (x) + 1.0 * (y) * (y))
using namespace std;
typedef long long ll;
int T, n, m;
int main() {
scanf("%d", &T);
while (T--) {
cin>>n>>m;
if (__gcd(n, m) == 1) {
printf("%.10lf\n", dis(n, m));
continue;
}
double ans = 1e18;
if (n > m) swap(n, m);
for (int i = 1, j; i <= n; i++) {
j = (1ll * m * i - 1) / n;
ans = min(ans, dis(i, j) + dis(n - i, m - j));
}
printf("%.10lf\n", ans);
}
return 0;
}
C题 数位dp,中档
题意:
给,求 的值,答案对 取模
分析:
由于,两个数字二进制每一位最多不可能同为1,因此不会产生进位;因此 的含义是 和 中最高位的1的位置,可以用数位dp做;
【状态表示】
len 是 i 和 j 的最高位1的位置;
a 和 b 分别表示 i 是否等于X,j 是否等于Y;
综上dp[len][a][b] 的含义为满足以下条件的 个数:
①i和j二进制最高位是len,即i和j的二进制高于len的数位都是0;
②a==[i==X], b==[j==Y];
③ i&j==0;
设X的二进制有 lenX 位,Y的二进制有 lenY位,则所求答案为:
意思是所有先枚举,k求以i为最高位,且最高位是第k位的(i,j)数量;再枚举k,求以j为最高位,且最高位是第k位的(i,j)数量。
【状态转移】
将 最高位是第k位的状态 转移为 最高位为k-1的状态答案之和;即子状态的 len 一定是 k-1;
讨论子状态的a'和b':
首先a'和b'一定可以为0;
若a==1 并且X的第 k 位是1,则a'可以为1;
若b==1 并且Y的第 k 位是1,则b'可以为0;
具体的转移技巧见代码;
转移的时候k从大到小记忆化搜索即可;
代码:
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const ll mod = 1e9 + 7;
const int N = 50;
ll dp[N][2][2];
//dp[len][j][k]:从第len位开始数,i是否上限,j是否上限时i&j==0的个数
int digitX[N], digitY[N];
ll a, b;
ll dfs(int len, bool limita, bool limitb) {
if (len == 0) return 1;
if (dp[len][limita][limitb] != -1) return dp[len][limita][limitb];
int upa = limita ? digitX[len] : 1;
int upb = limitb ? digitY[len] : 1;
ll res = 0;
for (int ii = 0; ii <= upa; ii++) {//ii是i的第k位,jj是j的第k位,为0或1
for (int jj = 0; jj <= upb; jj++) {
int x = ii & jj;
if (x == 1) continue;
res = (res + dfs(len - 1, limita && ii == upa, limitb && jj == upb)) % mod;
}
}
return dp[len][limita][limitb] = res;
}
ll cal(ll a, ll b) {
memset(digitX, 0, sizeof digitX);
memset(digitY, 0, sizeof digitY);
int lenX = 0, lenY = 0;
while (a) digitX[++lenX] = (a & 1), a >>= 1;
while (b) digitY[++lenY] = (b & 1), b >>= 1;
ll res = 0;
memset(dp, -1, sizeof dp);
for (int k = lenX; k >= 1; k--) {//i的最高位确定Log2(i+j)+1的值,即:i的第k位为1,j的第k位为0
res += (dfs(k - 1, k == lenX, k > lenY) * k) % mod;
res %= mod;
}
memset(dp, -1, sizeof dp);
for (int k = lenY; k >= 1; k--) {//j的最高位确定Log2(i+j)+1的值,即:i的第k位为0,j的第k位为1
res += (dfs(k - 1, k > lenX, k == lenY) * k) % mod;
res %= mod;
}
return res;
}
int main(void) {
int t;
scanf("%d", &t);
while (t--) {
scanf("%lld%lld", &a, &b);
printf("%lld\n", cal(a, b));
}
}
K题 lca,dfs,较难
题意:
无向图,每个节点有状态L或H,0号节点一定是L。从0出发,每到达一个节点u,u的状态将被翻转。要求不能连续经过两个状态相同的节点,并且无限走下去。问该图能否满足要求。
分析:
简称 “不连续经过相同状态节点 ”的路径为好路径,两端点初始状态不同的边为异色边。
该图能满足要求充要条件:存在一个点x,从起点有一条好路径到达x,且从x出发存在一条好路径能回到x,并且返回x的上一个节点的初始状态与x的初始状态相同,即x位于一个奇环。简而言之,从起点出发有一条好路径,终点x与该路径中某点u同色,并且x与u相连。
方法:
用异色边建图G。对于每条同色边(u,v)检查图G中是否存在0~u~v或0~v~u的简单路径,如果存在的话那么添加上这条同色边后就满足原要求;
判断是否存在上述简单路径的办法是:预处理图G的双连通分量,预处理图G的dfs树的所有lca。每次查询u,v时,设u,v的lca为w,若u,v,w在图G的同一个点双联通分量,则满足要求。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2e5 + 10;
int head[MAXN], ver[MAXN << 1], Next[MAXN << 1], tot;
inline void add(int x, int y) {
ver[++tot] = y;
Next[tot] = head[x];
head[x] = tot;
}
struct E_DCC {//双联通分量分解
int dfn[MAXN], low[MAXN], num, c[MAXN], dcc;
bool bridge[MAXN << 1];
inline void init(int n) {
num = dcc = 0;
memset(bridge, false, sizeof(bool) * (tot + 1));
memset(dfn, 0, sizeof(int) * (n + 1));
memset(c, 0, sizeof(int) * (n + 1));
tarjan(1, 0);
for (int i = 1; i <= n; i++)
if (!c[i] && dfn[i])
++dcc, dfs(i);
}
void tarjan(int x, int in_edge) {
dfn[x] = low[x] = ++num;
for (int i = head[x]; i; i = Next[i]) {
int y = ver[i];
if (!dfn[y]) {
tarjan(y, i);
low[x] = min(low[x], low[y]);
if (low[y] > dfn[x])
bridge[i] = bridge[i ^ 1] = true;
}
else if (i != (in_edge ^ 1))
low[x] = min(low[x], dfn[y]);
}
}
void dfs(int x) {
c[x] = dcc;
for (int i = head[x]; i; i = Next[i]) {
int y = ver[i];
if (c[y] || bridge[i]) continue;
dfs(y);
}
}
} E;
struct LCA {//查询LCA
int t, f[MAXN][20], d[MAXN];
inline void init(int r, int n) {
memset(d, 0, sizeof(int) * (n + 1));
memset(f[1], 0, sizeof(f[1]));
d[r] = 1, t = log2(n), dfs(r);
}
void dfs(int x) {
for (int i = head[x], y; i; i = Next[i]) {
if (d[y = ver[i]])continue;
d[y] = d[x] + 1, f[y][0] = x;
for (int j = 1; j <= t; j++)
f[y][j] = f[f[y][j - 1]][j - 1];
dfs(y);
}
}
inline int lca(int x, int y) {
if (d[x] > d[y])swap(x, y);
for (int i = t; i >= 0; i--)
if (d[f[y][i]] >= d[x])
y = f[y][i];
if (x == y)return x;
for (int i = t; i >= 0; i--)
if (f[x][i] != f[y][i])
x = f[x][i], y = f[y][i];
return f[x][0];
}
} L;
int T, n, m;
vector<pair<int, int>> edge;
char c[MAXN];
inline bool check(int x, int y) {
if (!E.dfn[x] || !E.dfn[y])return false;
int lca = L.lca(x, y);
if (lca == x || lca == y)return true;
return E.c[L.f[lca][0]] == E.c[x] || E.c[L.f[lca][0]] == E.c[y];
}
int main() {
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
scanf("%s", c + 1);
memset(head, 0, sizeof(int) * (n + 1));
tot = 1, edge.clear();
for (int i = 1, x, y; i <= m; i++) {
scanf("%d%d", &x, &y), x++, y++;
if (c[x] != c[y])add(x, y), add(y, x);
else edge.emplace_back(x, y);
}
E.init(n), L.init(1, n);
bool flag = false;
for (auto e : edge)
if (check(e.first, e.second)) {
flag = true;
break;
}
puts(flag ? "yes" : "no");
}
return 0;
}
F题 数学,dp,较难
题意:
又是特别长的一大段题干,读到最后面的Formally才发现前面都是废话……拿到题应该先扫一眼下面有没有Formally, In a word 这样的词;数学题就直接贴图片了。
分析:
先贴一下知乎上的一个分析,以后再写代码