一篇来自ACM入门者的补题记录
A.机器人
题意:有两条平行线段A,B,长度从1~n,机器人一开始位于A线段的s点,有r个目标点分布在两个线段上,同时存在m个特殊点,1,n是两个特殊点,机器人只能在特殊站点完成调头、区域间的转换,问机器人经过所有目标点并回到起点,需要的最短时间?
思路:因为起点与终点是同一个点,肯定需要画一个矩形把两个线段上离1点最近的目标点,和离n点最近的目标点给框起来(因为1,n是特殊点,这样的矩形必然存在)在一些情形下还需要画一些别的路线就可以走完所有特殊点。综合结论,只有两个因素值得考虑:
1.特殊点分布在s点的一侧还是两侧:
如果分布在两侧直接画大矩形;如果分布在一侧,考虑s点最远端的目标点,先画一条直路线连接,考虑第二个因素。
2.B线段是否存在目标点:
如果不存在,机器人在到达最远端的目标点以后,会找一个最近的特殊站点调头,重复之前走过的路程就好;如果存在,机器人会在特殊站点转换区域,接下来需要找的是B段离s最近的目标点的特殊点,让机器人通过此站点回到A段上;A段无需特判,B段需要特判,否则有可能让机器人在没有必要的情况下,绕到没有目标点的一端回去。
题目给的数据比计较水,但自己写的代码就是过不了,以下代码参考了别人的写法,lower_bound()和upper_bound()在这题中真的巨好用,别忘了在区域转移时加上转移时间k。
#include<bits/stdc++.h>
using namespace std;
int n,r,m,k,s;
vector<int>A;
vector<int>B;
vector<int>T;
int main()
{
scanf("%d%d%d%d%d",&n,&r,&m,&k,&s);
for(int i = 0;i<r;i++){
int w,flag;
scanf("%d%d",&w,&flag);
if(flag == 0 && w == s)
continue;
if(flag == 0)
A.push_back(w);
else if(flag == 1)
B.push_back(w);
}
T.push_back(1);
T.push_back(n);
for(int i = 0;i<m;i++){
int w;
scanf("%d",&w);
T.push_back(w);
}
sort(T.begin(),T.end());
sort(A.begin(),A.end());
sort(B.begin(),B.end());
int ans = 0;
int lb,rb;
if(B.empty() && A.empty()){
cout<<0<<endl;
return 0;
}
int MIN = 0x3f3f3f3f;
int MAX = 0;
if(!A.empty()){
MIN = min(MIN,A[0]);
MAX = max(MAX,A[A.size()-1]);
}
if(!B.empty()){
lb = B[0];
rb = B[B.size()-1];
MIN = min(MIN,B[0]);
MAX = max(MAX,B[B.size()-1]);
}
if(s <= MIN ){
int s1 = *(lower_bound(T.begin(),T.end(),MAX));
ans = s1 - s;
if(!B.empty()){
int s2 = *(upper_bound(T.begin(),T.end(),lb)-1);
ans += s1 - s2;
ans += abs(s2-s);
ans += 2*k;
}
else
ans += s1-s;
}
else if(s >= MAX){
int s1 = *(upper_bound(T.begin(),T.end(),MIN)-1);
ans = s - s1;
if(!B.empty()){
int s2 = *(lower_bound(T.begin(),T.end(),MAX));
ans += s2 - s1;
ans += abs(s-s2);
ans += 2*k;
}
else
ans += s - s1;
}
else{
int s1 = *(upper_bound(T.begin(),T.end(),MIN)-1);
int s2 = *(lower_bound(T.begin(),T.end(),MAX));
ans = 2*(s2-s1);
if(!B.empty())
ans += 2*k;
}
cout<<ans<<endl;
return 0;
}
B.吃豆豆
题意:n行m列的格子图,对于第i行第j列的格子,会在T[i][j]时刻出现一个糖果,问从S点走到T点,至少拿到C个糖果的最短时间是多少。
思路:
d
p
[
i
]
[
j
]
[
k
]
dp[i][j][k]
dp[i][j][k]代表到达i,j这个点花费k时间能拿到的最大糖果数,则状态转移方程为:
d
p
[
i
]
[
j
]
[
k
]
=
m
a
x
dp[i][j][k] = max
dp[i][j][k]=max{
d
p
[
四
个
方
向
]
[
k
−
1
]
dp[四个方向][k-1]
dp[四个方向][k−1]}
+
k
+ k
+k%
T
[
i
]
[
j
]
T[i][j]
T[i][j] ==
0
?
1
:
0
0 ? 1 : 0
0?1:0;
这是一个需要三重循环处理的dp,i,j的范围显而易见,k的范围考虑题目给了3000ms,n,m<=10,大胆将k设置为10010,找到k最小的
d
p
[
i
]
[
j
]
[
k
]
>
=
C
dp[i][j][k]>=C
dp[i][j][k]>=C就过了…
值得注意初始化要将起点的
d
p
[
i
]
[
j
]
[
k
]
dp[i][j][k]
dp[i][j][k]设置为0,其他的dp值设置为负无穷,以示从这个点开始才能获取到正常的糖果数…
代码不贴了,提一嘴div1的数据范围比div2的大得多,需要使用倍增法blabla…讲解没听太懂,哎…
C.拆拆拆数
题意:读入A,B两个数(5<=A,B<=1e18),把A拆为a1,a2…an,把B拆为b1,b2…bn,使得对于任意i,j∈[1,n],gcd(ai,bj) = 1;输出n最小的一组a与b。
思路:冷静分析一番,如果A,B存在这样分解方法,那么n一定<=2。当A,B互素时,直接输出n=1,A,B;当A,B不是互素的时候,想办法构造这样的一组a,b,很容易就想到了素数表,拿A,B去减同一个素数,如果剩下的数是互素的,就拆解完成了。
这是一道我现场过的题目,把素数表打出来后,随便搜搜就出A了,代码和思路一样很简单,就不贴了…别的博客有真正构造的方法(捂脸…)
D.超难的数学题
暂未补上…
E.流流流动
题意:有1,2,3…n共n个数字。取得数字i会有f[i]的收益。i为奇数时,与3*i+1之间有边,i为偶数时,与i/2之间有边。当一条边上的两点x,y同被取得时,会失去d[min(x,y)]的价值,求问最大收益。
思路:如果能看出这是棵树,这题就很好做了。把所有的边连起来,发现会出现一些三叉结点,处理方法是把三叉结点连接的两棵子树,合并成为同一棵树,用并查集去维护这个森林。因为不知道连接前后父子关系的转变,所以我们需要建立双向边。建好森林以后,从若干个根结点开始,分别进行很裸的树形dp就好了。
小坑点:1,2,4这三个点,4本身是一个三叉结点,但它所连接的子树1,2之间存在有边,建树以后就构成了一个循环节,所以我们在建树的时候需要跳过1这个点,舍弃1,4这条边,然后就能A了…
这是我写的第一道树形dp的题目,还是比较愉快的。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 105;
vector<int>tree[maxn];
vector<int>root;
int dp[maxn][2],f[maxn],d[maxn],ans;
int pre[maxn];
void di(int x,int fa){
dp[x][0] = 0;
dp[x][1] = f[x];
for(int i = 0;i<tree[x].size();i++){
int y = tree[x][i];
if(y == fa)
continue;
di(y,x);
dp[x][0] += max(dp[y][0],dp[y][1]);
dp[x][1] += max(dp[y][0],dp[y][1]-d[min(x,y)]);
}
}
int Find(int x){
if(x != pre[x])
return pre[x] = Find(pre[x]);
return x;
}
void init(int n){
for(int i = 1;i<=n;i++)
pre[i] = i;
for(int i = 2;i<=n;i++){
if(i % 2 == 1 && i * 3 + 1 <=n){
tree[i].push_back(i*3+1);
tree[i*3+1].push_back(i);
if(Find(i) != Find(i*3+1)){
int x = pre[i*3+1];
int y = pre[i];
pre[x] = y;
}
}
if(i % 2 == 0){
tree[i].push_back(i/2);
tree[i/2].push_back(i);
if(Find(i) != Find(i/2)){
int x = pre[i];
int y = pre[i/2];
pre[x] = y;
}
}
}
}
int main()
{
int n;
scanf("%d",&n);
for(int i = 1;i<=n;i++)
scanf("%d",&f[i]);
for(int i = 1;i<=n;i++)
scanf("%d",&d[i]);
init(n);
for(int i = 1;i<=n;i++)
if(pre[i] == i)
root.push_back(i);
ans = 0;
for(int i = 0;i<root.size();i++){
int x = root[i];
di(x,x);
ans += max(dp[x][0],dp[x][1]);
}
cout<<ans<<endl;
return 0;
}
F.爬爬爬山
一道比较裸的最短路题目,本场训练的签到题,队友做出来了。我还没有写过,不知道以后会不会写…hh…
———————————————————————————————————————
最近复习最短路,把这题给过了。题意比较裸,就是在爬山的时候考虑体力的变化,若体力不足则使用降低山高度的操作。因为每座山的高度只能被降低一次,所以我们只有在以一座山作为最短路起点时,才更新山的高度,需要记录一下这座山的高度是否发生过改变。
注意数据需要开 long long ,以及 n <=1e5,普通版本的迪杰特斯拉是过不了的,需要堆优化也就是优先队列版本的。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5+5;
const long long INF = 1e18;
int height[maxn];
long long dis[maxn],fis[maxn],rest[maxn];
bool vis[maxn];
int n,m,k;
struct edge{
int next,w;
edge(int a,int b){
next = a;
w = b;
}
};
vector<edge>G[maxn];
struct D{
int u;
long long d;
D(int a,long long b){
u = a;
d = b;
}
};
struct cmp{
bool operator()(D X,D Y){
return X.d > Y.d;
}
};
long long di(){
for(int i = 1;i<=n;i++){
fis[i] = 0;
vis[i] = false;
dis[i] = INF;
}
priority_queue<D,vector<D>,cmp> U;
rest[1] = k;
dis[1] = 0;
U.push(D(1,0));
while(!U.empty()){
int u = U.top().u;
long long d = U.top().d;
U.pop();
if(dis[u] < d)
continue;
if(u == n)
return d;
vis[u] = true;
height[u] -= fis[u];
int len = G[u].size();
for(int i = 0;i<len;i++){
int v = G[u][i].next;
int qe = G[u][i].w;
if(vis[v])
continue;
long long lo,nk,l = 0;
if(height[v]<=height[u]+rest[u]){
lo = 1ll*qe;
nk = height[u]+rest[u]-height[v];
}
else{
l = height[v]-height[u]-rest[u];
lo = qe+l*l;
nk = 0;
}
if(lo + d < dis[v]){
fis[v] = l;
dis[v] = lo + d;
rest[v] = nk;
U.push(D(v,dis[v]));
}
}
}
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
for(int i = 1;i<=n;i++)
scanf("%d",&height[i]);
for(int i = 0;i<m;i++){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
G[x].push_back(edge(y,z));
G[y].push_back(edge(x,z));
}
long long ans = di();
printf("%lld\n",ans);
return 0;
}
G.双重矩阵
暂未解决。
H.我爱割葱
据说是一道比较难的区间dp题目,区间dp的题目我尚停留在石子归并阶段,这题以后是一定要补的…
I.起起落落
题意:给定一个数组
p
n
p_n
pn,在[1,2,3…n]这个序列中,有多少个持续下降的子序列,定义持续下降的子序列
a
1
.
.
.
a
2
m
+
1
a_1...a_{2m+1}
a1...a2m+1 为:
1.
a
1
<
a
2
<
.
.
.
<
a
2
m
+
1
<
=
n
a_1<a_2<...<a_{2m+1}<=n
a1<a2<...<a2m+1<=n;
2.对于任意的
k
k
k,有
p
[
a
[
2
k
−
1
]
]
>
p
[
a
[
2
k
+
1
]
]
>
p
[
a
[
2
k
]
]
p[a[2k-1]]>p[a[2k+1]]>p[a[2k]]
p[a[2k−1]]>p[a[2k+1]]>p[a[2k]] 成立。
思路:这题其实光看题意是不好理解的,如果拿样例来推一推,就会发现我们需要找的子序列满足的,是一个呈破浪形下降的折线。
令
d
p
[
i
]
dp[i]
dp[i]为以ai结尾的持续下降子序列的个数,则
d
p
[
i
]
dp[i]
dp[i]的值是与ai后面的值无关的,考虑
j
∈
[
1
,
i
−
1
]
j∈[1,i-1]
j∈[1,i−1],若
a
j
a_j
aj小于
a
i
a_i
ai,则
a
j
a_j
aj可以作为以ai为结尾的持续下降子序列的中间节点;若
a
j
a_j
aj大于
a
i
a_i
ai,则代表
a
i
a_i
ai与其中间结点可以接在以
a
j
a_j
aj为结尾的中间结点的后面,表达式为:
d
p
[
i
]
+
=
(
d
p
[
j
]
+
1
)
∗
k
dp[i] += (dp[j]+1)*k
dp[i]+=(dp[j]+1)∗k 。
k
k
k代表符合条件的中间结点个数,加1是因为
a
j
a_j
aj,
k
k
k,
a
i
a_i
ai,其本身也是一种持续下降子序列的方案。
a i a_i ai和中间结点直接这样接 a j a_j aj在后面会满足持续下降子序列的条件吗?满足,因为题目要求的 2 k 2k 2k是一个偶数,我们的子序列每次都是2个2个拼接上去的,一定满足持续下降的条件。
#include<bits/stdc++.h>
using namespace std;
const int mod = 1e9+7;
int main()
{
int n;
scanf("%d",&n);
int p[n+1];
for(int i = 1;i<=n;i++)
scanf("%d",&p[i]);
long long dp[n+1];
memset(dp,0,sizeof(dp));
long long ans = 0;
for(int i = 3;i<=n;i++){
int k = 0;
for(int j = i-1;j>=1;j--){
if(p[j] < p[i])
k++;
else if(p[j] > p[i])
dp[i] += (dp[j]+1)*k%mod;
}
ans = (ans + dp[i])%mod;
}
cout<<ans<<endl;
return 0;
}
J.夺宝奇兵
题意:n个人手上持有m个宝物,第i个宝物的价格为 a i a_i ai,问需最少要准备多少金币将宝物买过来使自己能成为拥有宝物最多的人。
思路:一个看上去比较暴力的做法,枚举每个居民手中剩下0个宝物,1个宝物,m/2个宝物,如果自己拥有的宝物不是最多的,再将一些没买的宝物买过来,比较这多种方案,取最小花费。
这题是cf的原题,队友现场做的,但因为数据有很大的问题,队友怎么敲都没过,数据修正后一发A的。赛后请教队友补的这道题,真的是一种很巧妙的思想,学习到了。
K.星球大战
暂未解决。