关于考试:
好久没考试,都快没考试的感觉了…
终于在今天开始了愉快的暴0生活…
今天刚开始考,发现第1题是贪心水题之后,整个人就放松了,然而结果却是水题WA掉了…(考后有种欲哭无泪的感觉,以后看到水题还是要认真码啊…)
接着就是第二题,看完题解后感觉很简单,但是关于树的知识点都忘得差不多了,本来一道重心水题,结果我还是找规律+归纳法+…一阵乱搞,最后才找出来…(看来以后还是要多复习一下以前的知识…“好像现在连并查集、平衡树、STL都忘得差不多了…”)
最后就是第三题,看半天都没怎么找到思路,数论和组合数学太差,算法都想出来了,但却止步于和组合数学结合来降低时间复杂度…
下面附上题和代码
Task 1: 隔膜
问题描述:
steam夏季大促销来啦,azui大爷最近在steam上买了1mol的游戏。一天他突然发现了一个搬砖的游戏: 有N种砖头,每种砖头有mi个,每一个的价值为di。每一个单位时间你必须搬一块砖,到无砖可搬为止。有一个得分系数F,初始时为1。搬一块砖的得分为当时的得分系数F*di。有T个时间分割点。每过一个时间分割点,F会自己加1。 例如在时间pi的得分为i*di,而在时间pi+1的得分为(i+1)*di。
azui大爷觉得这个游戏too simple,不想去玩,于是让你去帮他上分,希望你能告诉他每局游戏的最大的分。
你一定知道这么简单的题目怎么做,快帮帮azui大爷吧。
输入:
第一行一个数N。
接下来的N行,每行两个数,表示mi和di。
之后的一行一个数T。
接下来的一行T个数,pi。
输出:
一个数,表示最大得分
样例输入:
1
1 1000
1
1
样例输出:
1000
数据范围:
对于30%的数据,1<=N<=10
对于100%的数据,
1≤N≤100
1≤mi≤109
0≤di≤1000
1≤t≤100
1≤p2<p3<…≤pt1012
解题思路:
我们可以先假设有两种不同的砖的权值分别为a,b(a<b),可以在两个不同时间段搬,时间段所对应的权值分别为c,d(c<d)。
那么我们此时可以得到两种不同搬运方式的最大分数分别是:①a*c+b*d, ②a*d+b*c。
通过做差(①-②)我们可得:(b-a)*(d-c),又∵b<t;a,d<t;c ∴原式>0 即:①>②。
因为时间的权值随着时间的递增而递增,所以要是分数最大,那么应该让砖的权值小的在前面。这样就是一道简单的贪心了。
Code:
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long LL;
const int MAXN = 105;
int n, t;
LL ans, p;
struct Node{
int m, d;
bool operator < (const Node &x) const{
return d < x.d;
}
}arr[MAXN];
int main(){
scanf("%d",&n);
for(int i = 1; i <= n; ++ i)
scanf("%d%d",&arr[i].m,&arr[i].d);
sort(arr+1, arr+n+1);
scanf("%d",&t);
LL lst = 0, used = 0; int pos = 1;
for(int i = 1; i <= t; ++ i){
scanf("%lld",&p);
LL tmp = p;
p -= lst, lst = tmp;
while(pos <= n && p){
if(p >= (arr[pos].m-used)){
ans += 1ll*i*(arr[pos].m-used)*(arr[pos].d);
p -= (arr[pos].m-used), used = 0, pos ++;
}
else{
ans += 1ll*i*p*arr[pos].d;
used += p, p = 0;
}
}
}
if(used) ans += 1ll*(t+1)*(arr[pos].m-used)*arr[pos].d, pos ++;
while(pos <= n){
ans += 1ll*(t+1)*arr[pos].m*arr[pos].d;
pos ++;
}
printf("%lld",ans);
return 0;
}
Task 2: 快递配对
问题描述:
azui大爷厌倦了每天在家颓废的生活,于是开始打工送快递。Jeremy同学不想让azui大爷太轻松,于是想让他送快递的路程尽可能的长。
一句话来说就是:
给出一棵n个点的树,将这n个点两两配对,求所有可行的方案中配对两点间的距离的总和最大为多少。
输入:
一个数n(1<=n<=100,000,n保证为偶数)
接下来n-1行每行三个数x,y,z表示有一条长度为z的边连接x和y(0<=z<=1,000,000,000)
输出:
一个数表示最大的距离总和。
样例输入:
6
1 2 1
1 3 1
1 4 1
3 5 1
4 6 1
样例输出:
7
数据范围:
对于20%的数据,n <= 14
对于另20%的数据,所给的树是一条链。
对于100%的数据,n <= 100000
解题思路:
【概念补充】树的重心:树的重心也叫树的质心。找到一个点,其所有的子树中最大的子树节点数最少,那么这个点就是这棵树的重心,删去重心后,生成的多棵树尽可能平衡。
首先,有这样一个结论:如果要使距离总和最大,那么任意两个匹配点的路径一定经过树的重心。
证明:
假设当前我们有至少一组匹配的路径没有经过重心。
这里我们分开讨论路径未经过重心的匹配组数为奇和偶时的情况:
Case 1:如果组数为偶数,那么,显而易见的,这些匹配会独立分散在以重心为根节点的子树当中。那么,我们就可以将这些匹配先拆开,然后分别与其他子树中拆开的节点相连,因为开始时这些匹配都未经过重心,那么就会比原来多出“每一个拆开节点到中心的距离减去原先这些匹配的距离和”的贡献。因为拆开的匹配开始都存在于重心的子树中,所以对于任意匹配而言,其的距离一定包含于其到重心的距离和,即:dis(i,j) <= dis(i,rt)+dis(j,rt)。
所以,证得当组数为偶数时,结论成立。
Case 2:如果组数为奇数,大致与偶数是相同,这里就不再赘述了。
证完之后,我们就可以直接找树的重心了,如果忘了怎么找重心,可以看一下,下面这段代码。
int pos, cmp;
bool vis[MAXN];
int dfs_find(int u){
vis[u] = 1;
int siz = 1, Max = 0, tmp;
for(int i = fir[u]; i; i = Edges[i].nxt){
if(vis[Edges[i].e]) continue;
tmp = dfs_find(Edges[i].e);
if(tmp > Max) Max = tmp;
siz += tmp;
}
if(n-siz > Max) Max = n-siz;
if(Max < cmp) pos = u, cmp = Max;
return siz;
}
下面是整道题的代码
Code:
#include <cstdio>
#include <cstring>
const int MAXN = 100005;
int n;
inline void Read(int &Ret){
char ch;
while(ch = getchar(), ch < '0' || ch > '9');
Ret = ch - '0';
while(ch = getchar(), ch >= '0' && ch <= '9')
Ret = Ret * 10 + ch - '0';
}
int fir[MAXN], ecnt = 1;
struct Node{int e, v, nxt;}Edges[MAXN<<1];
inline void AddEdge(int u, int e, int v){
Edges[++ecnt].e = e;
Edges[ecnt].v = v;
Edges[ecnt].nxt = fir[u];
fir[u] = ecnt;
Edges[++ecnt].e = u;
Edges[ecnt].v = v;
Edges[ecnt].nxt = fir[e];
fir[e] = ecnt;
}
int pos, cmp;
bool vis[MAXN];
int dfs_find(int u){
vis[u] = 1;
int siz = 1, Max = 0, tmp;
for(int i = fir[u]; i; i = Edges[i].nxt){
if(vis[Edges[i].e]) continue;
tmp = dfs_find(Edges[i].e);
if(tmp > Max) Max = tmp;
siz += tmp;
}
if(n-siz > Max) Max = n-siz;
if(Max < cmp) pos = u, cmp = Max;
return siz;
}
long long ans;
void dfs_calc(int u, long long dis){
vis[u] = 1, ans += dis;
for(int i = fir[u]; i; i = Edges[i].nxt){
if(vis[Edges[i].e]) continue;
dfs_calc(Edges[i].e, dis+Edges[i].v);
}
}
int main(){
Read(n);
int a, b, c;
for(int i = 1; i < n; ++ i){
Read(a),Read(b),Read(c);
AddEdge(a, b, c);
}
pos = 1, cmp = n;
dfs_find(pos);
memset(vis, 0, sizeof vis);
dfs_calc(pos, 0);
printf("%lld\n",ans);
return 0;
}
Task 3:子集
问题描述:
azui大爷在quack大爷的带领下开始玩集合了,可是他太懒了,不想做quack大爷布置的作业题,便拿来给你做了:S 集合中有n个不同的元素,我们从1-n标号。考虑S 的子集Si,j,将这些子集排成一个r行c列矩阵的样子。其中第一行为S1,1,S1,2,…,S1,c,第二行为S2,1,S2,2,..,S2,c一直到第r行为Sr,1, Sr,2,…, Sr,c。这些集合还满足对于在一行中左右相邻的两个集右,左侧是右侧的子集,即Si,j∈Si,j+1。这些集合还满足对于在一列中上下相邻的两个集合,上方是下方的子集,即Si,j∈Si+1,j。问对于S 的全部子集,有多少可能的情况排成上述的矩阵(每个子集可以重复使用),结果模109 +7 输出。你一定知道这么简单的题目怎么做,快帮帮azui大爷吧。
输入:
一行三个数n,r,c.
输出:
一个数表示答案。
样例输入:
1 2 2
样例输出:
6
样例解释:
1 2 3 4 如上图标号后,有:1是2和3的子集,1,2,3都是4的子集。
用0表示空集,1表示元素1。
有以下6种情况:
00 | 00 | 00 | 01 | 01 | 11
00 | 01 | 11 | 01 | 11 | 11
数据范围:
对于10%的数据,r*c*n <= 16
对于20%的数据,r*c <= 16, n <= 100
对于另20%的数据,r=1, c<=1000000
对于另10%的数据,n=1
对于100%的数据,r, c <= 1000000, n <= 10^9
解题思路:
乍一看去,这道题好像毫无思路,但我们可以简化条件。
在简化条件时,我们首先可以发现,这里无论有多少种不同的元素,但其对于答案的贡献都是不会相互影响的。那么根据乘法原理,我们就可以知道最后的答案就是先计算出当n=1时的方案数,然后有k种元素,就k次方就行了。
那么,我们就重点看一下当n=1时的方案数。
如果我们把任一,一种方案先用0、1在一个矩阵中表示出来,我们就不发现:如果,在第i行第j列放上一个0的话,那么为了保证方案合法,就必须保证在以(1,1)为左上角,(i,j)为右下角的矩形中不存在1。这时,我们就能通过表格直观的发现,整张表格实际上被分为了在左上的0,和右下的1两个部分。
而中间分割0和1的折线的方案数就是n=1时的答案,根据组合数学,我们可以知道这里的方案就是C(n+r, c)。
Code:
#include<cstdio>
const int MOD=1e9+7;
inline int ksm(int a,int b){
long long ret=1,tmp=a;
while(b){
if(b&1) ret=ret*tmp%MOD;
tmp=tmp*tmp%MOD;b>>=1;
}
return ret%MOD;
}
inline int fac(int n){
int ret=1;
for(int i=2;i<=n;i++)
ret=(long long)ret*i%MOD;
return ret;
}
int inv(int x){
if(x==1) return 1;
return (long long)(MOD-MOD/x)*inv(MOD%x)%MOD;
}
int C(int n,int m){
return (long long)fac(n)*inv(fac(m))%MOD*inv(fac(n-m))%MOD;
}
int main() {
int n,r,c;
scanf("%d%d%d",&n,&r,&c);
printf("%d",ksm(C(r+c,r),n));
return 0;
}