【ACM-ICPC】Nowcoder Summer Training Camp 2
B. Cannon
题意
第一行x个炮棋子,第二行y个炮棋子,隔着吃子。f1(i)表示吃i个棋子的吃法数,f2(i)表示先吃第一行再吃第二行吃i个棋子的吃法数
问:f1(i)异或和、f2(i)异或和对p取模?(x,y范围5e6)
解法分析
首先,在最后每行剩下2个棋子,就是说我们有第一行x - 2
个操作,第二行y - 2
个操作。
那么f1(i)就是从x + y - 4
个操作中选择i个再进行排列,即 \(A_{x+y-4}^i\) 而且每个操作都有正反两遍,所以$$f1(i) = 2^i * A_{x+y-4}^i$$
第二问比较复杂,类似一问推出公式$$ f2(i) = 2^i * \sum_{k=0}i(A_{x-2}kA_{y-2}^{i-k})$$
将排列化为阶乘,最终得到$$f2(i) = 2^i * \frac{(x-2)!(y-2)!}{(x + y - i)!}\sum_{k=n-i}nC_{x+y-4-i}k$$
左边预处理阶乘和阶乘逆元,右边要O(1)转移,预处理组合数前缀和,采用步移算法推导。
代码
#include <cstdio>
using namespace std;
#define LL long long
const LL p = 1e9 + 9;
LL f1[10000010];
LL fac[10000010], inv[10000010];
LL sc[10000010];
LL quick_pow(LL x, LL y)
{
LL ans = 1;
while (y)
{
if (y & 1)
ans = ans * x % p;
x = x * x % p;
y >>= 1;
}
return ans;
}
LL C(LL x, LL y)
{
if (x < y || x < 0 || y < 0)
return 0;
return fac[x] * inv[y] % p * inv[x - y] % p;
}
int main()
{
int n, m;
scanf("%d %d",&n,&m);
n -= 2;
m -= 2;
LL ans1 = 0;
f1[0] = 1;
fac[0] = 1;
for (int i = 1; i <= n + m; ++i)
fac[i] = fac[i - 1] * i % p;
inv[n + m] = quick_pow(fac[n + m], p - 2);
for (int i = n + m; i >= 1; --i)
inv[i - 1] = inv[i] * i % p;
sc[n + m] = 1;
for (int i = n + m; i > 0; --i)
sc[i - 1] = sc[i] * 2 - C(n + m - i, n) - C(n + m - i, n - i),
sc[i - 1] = (sc[i - 1] % p + p) % p;
LL ans2 = 0, p2 = 1;
for (int i = 0; i <= n + m; ++i)
{
ans1 ^= p2 * fac[i] % p * C(n + m, i) % p;
ans2 ^= (LL)p2 * fac[n] % p * fac[m] % p * inv[n + m - i] % p * sc[i] % p;
p2 = p2 * 2 % p;
}
printf("%lld %lld\n",ans1,ans2);
return 0;
}
C. Draw Grids
题意
给出一个n * m
的点阵,两个人进行连线游戏,连线规则是:每个人选定一个点和距离它为1的另一个点连接(上下左右),A开始连,连线过程中不能出现封闭几何图形,最终无法连线者输。
解法分析
是签到题捏!n * m
的点阵,能连线n * m - 1
。
证明:以n * m
个点连成树考虑,任意加一条边都会成环。
代码
//咳咳。。。。
#include <bits/stdc++.h>
using namespace std;
int a[5][5];
int main()
{
for (int i = 1; i <= 4; ++i)
for (int j = 1; j <= 4; ++j)
a[i][j] = 1;
a[1][1] = a[1][3] = a[3][1] = a[3][3] = 0;
int n, m;
cin>>n>>m;
if (a[n][m])
cout<<"YES";
else
cout<<"NO";
return 0;
}
#include <bits/stdc++.h>
using namespace std;
int a[5][5];
int main()
{
int n, m;
cin>>n>>m;
if ((n * m - 1) & 1)
cout<<"YES";
else
cout<<"NO";
return 0;
}
小结
比赛的时候没想很多因为n
和 m
范围都是1 ~ 4。(直接打了表)
D. Er Ba Game
题意
二八游戏,按照规则判定赢家。
解法分析
模拟一下。
代码
//by hqy
#include <iostream>
typedef long long ll;
using namespace std;
ll t,a1,b1,a2,b2;
ll calc(ll x1,ll x2)
{
if(x1>x2)
{
ll tt=x1;
x1=x2;
x2=tt;
}
if(x1==2 && x2==8)
{
return 1000000;
}
else if(x1==x2)
{
return x1*10000;
}
else
{
return ((x1+x2)%10) * 100 + x2;
}
}
int main() {
cin>>t;
while(t--)
{
cin>>a1>>b1>>a2>>b2;
ll s1=0,s2=0;
if(a1>b1)
{
ll tt=a1;
a1=b1;
b1=tt;
}
if(a2>b2)
{
ll tt=a2;
a2=b2;
b2=tt;
}
s1=calc(a1,b1);
s2=calc(a2,b2);
if(s1>s2)cout<<"first\n";
else if(s1<s2)cout<<"second\n";
else
{
cout<<"tie\n";
}
}
return 0;
}
小结
采用计分方式
F. Girlfriend
题意
在三维空间中给定四个固定点A、B、C、D,两个不定点P1、P2,和比例系数k1、k2,满足
求P1和P2轨迹所形成两个立体图形的共同区域(交集)的体积
解法分析
数学知识告诉我们P点所形成的立体图形是一个球(参考阿波罗尼斯圆),所以问题转化为了,求出两圆心和半径之后的体积交,分三种情况:相离(答案为0),包含(小球体积),相交(采用积分得出公式进行计算)。
代码
//by jrt
#include <bits/stdc++.h>
#define fir first
#define sec second
using namespace std;
const double eps=1e-3;
typedef pair<int,int> pii;
struct po {
double x,y,z;
};
const double PI=acos(-1);
po A,B,C,D;
double k1,k2;
int main() {
double ans1,ans2,Tx;
int kase;
po p1,p2,p3,p4,o1,o2;
double r1,r2,L;
scanf("%d",&kase);
while(kase--) {
scanf("%lf%lf%lf",&A.x,&A.y,&A.z);
scanf("%lf%lf%lf",&B.x,&B.y,&B.z);
scanf("%lf%lf%lf",&C.x,&C.y,&C.z);
scanf("%lf%lf%lf",&D.x,&D.y,&D.z);
scanf("%lf%lf",&k1,&k2);
p1= {A.x+k1/(k1+1.0)*(B.x-A.x),A.y+k1/(k1+1.0)*(B.y-A.y),A.z+k1/(k1+1.0)*(B.z-A.z)};
p2= {A.x+k1/(k1-1.0)*(B.x-A.x),A.y+k1/(k1-1.0)*(B.y-A.y),A.z+k1/(k1-1.0)*(B.z-A.z)};
o1= {(p1.x+p2.x)/2.0,(p1.y+p2.y)/2.0,(p1.z+p2.z)/2.0};
r1=sqrt((o1.x-p1.x)*(o1.x-p1.x)+(o1.y-p1.y)*(o1.y-p1.y)+(o1.z-p1.z)*(o1.z-p1.z));
//printf("%lf %lf %lf %lf\n",o1.x,o1.y,o1.z,r1);
p3= {C.x+k2/(k2+1)*(D.x-C.x),C.y+k2/(k2+1)*(D.y-C.y),C.z+k2/(k2+1)*(D.z-C.z)};
p4= {C.x+k2/(k2-1)*(D.x-C.x),C.y+k2/(k2-1)*(D.y-C.y),C.z+k2/(k2-1)*(D.z-C.z)};
o2= {(p3.x+p4.x)/2.0,(p3.y+p4.y)/2.0,(p3.z+p4.z)/2.0};
r2=sqrt((o2.x-p3.x)*(o2.x-p3.x)+(o2.y-p3.y)*(o2.y-p3.y)+(o2.z-p3.z)*(o2.z-p3.z));
//printf("%lf %lf %lf %lf\n",o2.x,o2.y,o2.z,r2);
L=sqrt((o1.x-o2.x)*(o1.x-o2.x)+(o1.y-o2.y)*(o1.y-o2.y)+(o1.z-o2.z)*(o1.z-o2.z));
Tx=(r1*r1-r2*r2+L*L)/(2.0*L);
//printf("%lf %lf\n",L,Tx);
if(L>=r1+r2)printf("0.0\n");
else if(r1>=L+r2)printf("%.6lf\n",4.0/3.0*PI*r2*r2*r2);
else if(r2>=L+r1)printf("%.6lf\n",4.0/3.0*PI*r1*r1*r1);
else {
ans1=PI*r1*r1*(r1 - Tx) - (1.0/3.0)*PI*(r1*r1*r1 - Tx*Tx*Tx);
// printf("%lf %lf\n",ans1,PI*r1*r1*(r1 - Tx));
ans2=PI*r2*r2*(Tx-(L-r2)) + (1.0/3.0)*PI*((L-Tx)*(L-Tx)*(L-Tx) - r2*r2*r2);
printf("%.6lf\n",ans1+ans2);
}
}
return 0;
}
小结
中间由于不仔细除了些小差错,而且一开始忘记考虑相离、相包含情况,好在后来de出来了。
G. League of Legends
题意
n个区间\([a_i,b_i)\)要分成k组,求每组区间总交集的和的最大值。每组必须有一个区间,每组必须要有交集。无解输出0。
解法分析
对于能包含其他小区间的大区间,最优方案有两种:
- 单独成组,影响为少一个组,贡献为大区间总长。
- 放入自己能包含的区间内。影响贡献都无。
对于独立小区间,进行左端点排序,然后分组选取就成为连续的,若不连续肯定不会优于连续选取。考虑动态规划dp[i][j]表示排好后前j个区间分成i组的最优答案,dp转移方程:$$ dp[i][j] = max{dp[i - 1][t - 1] + b_t - a_j}$$其中t满足bt - aj > 0。
若暴力求解,时间复杂度为O(n^3),TLE。然后我们发现\(max\{dp[i - 1][t - 1] + b_t\}\)相当于滑动窗口取最大值,用单调队列优化,时间复杂度为O(n^2)。
然后大区间从大到小排序得到答案:
代码
#include <bits/stdc++.h>
using namespace std;
struct node
{
int l, r;
} a[5010], b[5010];
int len[5010], dp[5010][5010];
int q[5010];
bool my_cmp(struct node a, struct node b)
{
return a.r == b.r ? a.l > b.l : a.r < b.r;
}
bool my_cmp2(int a, int b)
{
return a > b;
}
int main()
{
int n, k, m1 = 0, m2 = 0;
cin >> n >> k;
for (int i = 1; i <= n; ++i)
cin >> a[i].l >> a[i].r;
sort(a + 1, a + n + 1, my_cmp);
// for (int i = 1; i <= n; ++i)
// cout << a[i].l<<' '<<a[i].r<<endl;
for (int i = 1; i <= n; ++i)
{
if (!m1 || b[m1].l < a[i].l)
b[++m1] = a[i];
else
len[++m2] = a[i].r - a[i].l;
}
memset(dp, -1, sizeof(dp));
dp[0][0] = 0;
for (int i = 1; i <= k; ++i)
{
int head = 1, tail = 0;
for (int j = 1; j <= m1; ++j)
{
if (~dp[i - 1][j - 1])//可入队
{
while (head <= tail && dp[i - 1][j - 1] + b[j].r >= dp[i - 1][q[tail] - 1] + b[q[tail]].r)
--tail;
q[++tail] = j;
}
while (head <= tail && b[q[head]].r <= b[j].l)
++head;
if (head <= tail)
dp[i][j] = dp[i - 1][q[head] - 1] + b[q[head]].r - b[j].l;
}
}
int ans = 0, sum = 0;
len[0] = 0;
sort(len + 1, len + m2 + 1, my_cmp2);
for (int i = 0; i <= min(m2, k); ++i)
{
sum += len[i];
if (~dp[k - i][m1])
ans = max(ans, dp[k - i][m1] + sum);
}
cout << ans;
return 0;
}
小结
I. Penguins
题意
两个20 * 20
的迷宫,A从迷宫1的(20,20)
走向(1,20)
,B从迷宫2的(20,1)
走向(1,1)
,A,B呈镜像走法:A向左,则B向右,但上下不变。问最短走法的最小字典序。
解法分析
显然,同一个局面不可能出现两次,用vis[a][b][c][d]
记录A到(a,b)
,B到(c,d)
的局面,然后按照“DLRU”的顺序bfs。
bfs队列长度最多20^4
代码
#include <bits/stdc++.h>
using namespace std;
bool vis[25][25][25][25];
struct node
{
int a, b, c, d, pre, dep;
char t;
}q[500 * 500];
char map1[25][25], map2[25][25];
void dfs(int x)
{
map1[q[x].a][q[x].b] = 'A';
map2[q[x].c][q[x].d] = 'A';
if (q[x].pre)
dfs(q[x].pre),
cout<<q[x].t;
}
int main()
{
for (int i = 1; i <= 20; ++i)
cin>> map1[i] + 1 >> map2[i] + 1;
for (int i = 1; i <= 20; ++i)
map1[i][0] = map2[i][0] = map1[i][21] = map2[i][21]
= map1[0][i] = map2[0][i] = map1[21][i] = map2[21][i] = '#';
int x, head = 1, tail = 1;
q[head] = (node){20, 20, 20, 1, 0, 0};
vis[20][20][20][1] = 1;
while (head <= tail)
{
int a = q[head].a, b = q[head].b, c = q[head].c, d = q[head].d;
if (a == 1 && b == 20 && c == 1 && d == 1)
{
x = head;
break;
}
int ta, tb, tc, td;
//D
if (map1[a + 1][b] == '#')
ta = a, tb = b;
else
ta = a + 1, tb = b;
if (map2[c + 1][d] == '#')
tc = c, td = d;
else
tc = c + 1, td = d;
if (!vis[ta][tb][tc][td])
{
vis[ta][tb][tc][td] = 1;
q[++tail] = (node){ta, tb, tc, td, head, q[head].dep + 1, 'D'};
}
//L
if (map1[a][b - 1] == '#')
ta = a, tb = b;
else
ta = a, tb = b - 1;
if (map2[c][d + 1] == '#')
tc = c, td = d;
else
tc = c, td = d + 1;
if (!vis[ta][tb][tc][td])
{
vis[ta][tb][tc][td] = 1;
q[++tail] = (node){ta, tb, tc, td, head, q[head].dep + 1, 'L'};
}
//R
if (map1[a][b + 1] == '#')
ta = a, tb = b;
else
ta = a, tb = b + 1;
if (map2[c][d - 1] == '#')
tc = c, td = d;
else
tc = c, td = d - 1;
if (!vis[ta][tb][tc][td])
{
vis[ta][tb][tc][td] = 1;
q[++tail] = (node){ta, tb, tc, td, head, q[head].dep + 1, 'R'};
}
//U
if (map1[a - 1][b] == '#')
ta = a, tb = b;
else
ta = a - 1, tb = b;
if (map2[c - 1][d] == '#')
tc = c, td = d;
else
tc = c - 1, td = d;
if (!vis[ta][tb][tc][td])
{
vis[ta][tb][tc][td] = 1;
q[++tail] = (node){ta, tb, tc, td, head, q[head].dep + 1, 'U'};
}
++head;
}
cout<<q[x].dep<<endl;
dfs(x);
cout<<endl;
for (int i = 1; i <= 20; ++i)
map1[i][0] = map2[i][0] = map1[i][21] = map2[i][21]
= map1[0][i] = map2[0][i] = map1[21][i] = map2[21][i] = 0;
for (int i = 1; i <= 20; ++i)
cout << map1[i] + 1 <<' '<< map2[i] + 1 <<endl;
return 0;
}
小结
暴力yyds
K. Stack
题意
模拟一个单调递增的单调栈,1 ~ n元素按照某种排列入栈,给定序列<ai,bi>,表示ai个元素入栈后栈中有bi个元素。问:是否有一种排列答案符合序列<ai,bi>?
解法分析
若不考虑元素取遍1 ~ n,考虑元素可重复情况,那么解法很容易,对于每个 <ai,bi>将ai取为bi,1取为1,其他未处理赋值为前一个+1,就是一个满足序列。然后考虑取遍1 ~ n,按照已经计算好的bi确定每个位置的优先级顺序,采用链式桶进行记录,从前向后遍历桶,每个桶中从后向前,能得到优先级顺序,然后得到每个数的位置,形成合法序列。
当然,排列不存在的情况就是 $$ b_1 != 1, b_i + 1 < b_{i+1} $$
代码
#include <bits/stdc++.h>
using namespace std;
int n, k;
int a[2333333], b[2333333], pos[2333333];
vector<int> bas[2333333];
int ans[2333333];
int main()
{
int flag = 1;
cin >> n >> k;
memset(pos, 0, sizeof(pos));
for (int i = 1; i <= k; ++i)
{
cin >> a[i] >> b[i];
if (a[i] == 1)
{
if (b[i] != 1)
flag = 0;
--i;
--k;
continue;
} //忽略1
pos[a[i]] = b[i];
}
pos[1] = 1;
for (int i = 1; i <= n; ++i)
{
if (!pos[i])
pos[i] = pos[i - 1] + 1;
if (pos[i] + 1 < pos[i + 1])
flag = 0;
bas[pos[i]].push_back(i);
}
if (!flag)
{
cout << -1;
return 0;
}
int now = 0;
for (int i = 1; i <= n; ++i)
{
for (int j = bas[i].size() - 1; j >= 0; --j)
ans[bas[i][j]] = ++now;
}
for (int i = 1; i < n; ++i)
cout << ans[i] << ' ';
cout << ans[n];
return 0;
}
小结
非常巧妙的做法。
比赛总结
这次比赛由于只有一台电脑,反而在一定程度上更促使我们之间充分交流,在许多题目上都是两个人同时讨论来确定算法,这种方式之后可以继续沿用。
流程
- 开局时,hqy看了A - D,lcj看了E - H,jrt看了I - L。
- 很快我们发现了签到题的C和D,分别由lcj和hqy完成。
- 然后jrt和lcj之前在分析K和F,后来两人提议交换分析,lcj提出阿波罗尼斯圆的思路,jrt完成了剩下题目的数学公式推导。
- 与此同时hqy和lcj推出了K的规律,jrt上机写F,由于上机提交有一些错误,加上hqy那里K已经完全推出,所以hqy上机完成了K。
- jrt再修改F,完成了F。
- 与此同时lcj和hqy分析了I广搜的复杂度分析,lcj写了I。
- jrt和hqy分析J题。再之后jrt上机写J题,但遇到2的次幂的取模问题始终无法解决。此时hqy和lcj分析G题,最后三人一起分析B题直到比赛结束。
(由jrt记录编写)