初见,安。
这篇博客的标题是一个月前zy就提出来的。现在终于派上用场了呢。
怎么就自闭了呢?因为想到了但是总有没想到的地方,或者写不出代码,或者是根本就没有往正确的方向去想……
题目的包装很棒啊,前两个都是那种第一反应得到的算法并不是正解的那种。
本以为第一题至少还是可以得个100凑个三位数的,结果就这样又考虑漏了一些东西,丢了20分。后面俩题都WA完了……
卡片游戏
传送门:入门OJ P2056
Description
有N只moreD在玩一个卡片游戏:
首先,桌子上有M张卡片,这M张卡片分别有一个唯一的1~M的编号。N只moreD在桌子上抢牌
。每个人最后的得分是所得的所有卡片编号的乘积(如果一张卡片都没取,得分为1)。
这N只moreD最后报出了自己的得分。你的任务是写一个程序,判断有没有人说谎。
Input
输入第一行一个整数T,表示T组测试数据。
对于每组测试数据:
第一行:两个用空格隔开的整数:N和M,表示moreD的数量和卡片的数量
第二行:有N个正整数Ai,表示每只moreD报出的得分。
N<=5 M<=100 Ai<=50000 t<=10
Output
输出T行,每行输出'Yes'或'No',表示'Yes'表示不可能没有人说谎 , 'No'表示可能没有人
说谎。
Sample Input
3
2 3
2 3
2 3
3 6
2 5
4 6
Sample Output
No
Yes
No
//对于第一个数据,存在第一个人抢到编号为2的卡片,第二个人抢到编号为3的卡片就可以满
足这样的情形了,所以可能没有人说谎。
对于第二个数据,不存在任何一种抢牌方案使得两人的得分满足这样的情形,所以不可能没
有人说谎。
Sol
先说一下80分的做法——因为很好想到,就是分解质因数,统计每个数分解下来各个质因数出现的次数再前缀和累加起来,再分解当前的每个人的得分,看是否有哪个质因数出现的次数大于我们预处理的结果,有则有人说谎。最后下来如果都没有超过的,那么就没有人说谎。
但这样的思路有一个漏洞,就是说如果分解过后剩下来的质因数并不能组成一个m以内的没有出现过的数呢?就比如这组数据:
1
2 7
36 70
,7以内各个质因数出现次数为:2——4次,3——2次,5和7各一次。也就是说最后会剩下一个2没有被用到,但是如果我把这两个数当做卡片拆开的话就是:
,会发现怎么都不能不重复地划分。所以其实是不合法的。也就是说我们只能每个数作为整体考虑。
那么正解怎么做呢——爆搜。【??!?!是的我没骗你,爆搜……你看n和m的范围这么小直接爆搜枚举当前这个人的得分能不能整除当前枚举到的卡片,能则除,递归,再递归一个不除的。
中途几乎不需要剪枝,特判一下如果答案已经确定为找得到合法解了,那就不用搜了。
#include<bits/stdc++.h>
using namespace std;
int T, n, a[10], m;
bool flag = false, vis[104];
void dfs(int x, int y, int ans) {//x为当前人,y为当前卡片,ans为当前人整除某些卡片过后剩下的部分
if(flag) return;//已经有正解了就不用搜了
if(x > n) {flag = true; return;}//顺利扫完了n个人,解合法。
if(y > m) {//卡片扫完了,那就从头再来……就算是O(nm)也不会爆
if(ans == 1) dfs(x + 1, 1, a[x + 1]);
return;
}
if(!vis[y] && ans % y == 0) {//这张卡片没有用过并且可以存在于这个人的得分中
vis[y] = true;
dfs(x, y + 1, ans / y);
vis[y] = false;//dfs还原
}
dfs(x, y + 1, ans);
}
int main() {
scanf("%d", &T);
while(T--) {
flag = false;
memset(vis, 0, sizeof vis);
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
dfs(1, 1, a[1]);
if(flag) puts("No");
else puts("Yes");
}
}
代码是参考了某篇博客,现在找不到了QwQ
泡泡浴
传送门:入门OJ P2057
Description
其实moreD是一个十分爱干净的孩子,最近moreD迷恋上了泡泡浴。迷恋泡泡浴的原因除了泡泡特别好玩之外,还由
于洗泡泡浴的浴缸十分奇怪,这是个地面参差不齐的浴缸。我们可以把浴缸看成N*M的矩阵,那么矩阵上的每个元
素为该位置浴缸的高度。由于水往低处流,相邻的格子的水面高度不同,高处的水会往低处去。浴缸当然要有出水
口,出水口在某个格子上。而且浴缸的边缘可以看作无穷高。然后大家肯定会发现,洗完泡泡浴后,把水放掉后,
可能有些地方会积水。即水不能通过出水口流出。moreD就是喜欢看看这些积水。moreD测量了一下,自己放水之前
浴缸的所有位置的水面高度均为K(除了浴缸底部高度大于K的格子)。如果浴缸底部高度大于K,那么这个格子没有
水,水面高度可以当作是浴缸底部高度。然后moreD打开了出水口。请你帮moreD预测一下,当出水口不再出水了,
浴缸每个格子的剩余的水高度是多少。(剩余的水的高度=水面高度-浴缸底部高度)
Input
输入第一行仅包含三个整数N,M,K,表示浴缸的大小与初始每个格子的水面高度。
接下来N行,每行M个整数,表示每个格子浴缸底部的高度A[i,j]。
接下来一行两个数r和c,表示出水口的行和列。
N,M<=800 A[I,j]<=10000
Output
输出N行,每行包含M个整数,表示每个格子的剩余水的高度。
Sample Input
3 3 66
6 9 1
7 8 1
6 8 1
3 2
Sample Output
2 0 7
1 0 7
2 0 7
Sol
第一反应——bfs!从排水口开始bfs!计算每个位置的水量!然后,样例过了,测试数据全部WA掉了……
我们这样考虑——对于每一个格子的水会被排多少,会剩下多少,我们就先从大局上只考虑最后每个格子相对与地面的水位线有多高,而并非对于每个格子的高度来说水位线有多高。这样的话,我们只需要得到每个格子的水位线,再减去这个格子的高度就可以了。
对于每个格子的水位线高度【相对与地面】,取决于所有路径上最高的那个格子的最矮值。因为对于每一条路径都可以看做是一维的,而高的格子会挡住去路,让水积蓄;如果有别的路径可以放水并且最高的那个格子的高度更低,那么最后剩的水就会取决于那个更低的路径。
所以我们需要维护的就是:从出水口到每个格子的所有路径上的最高点的最矮点。好像也可以直接bfs啊!!!但是比bfs还要偏向另一个算法——最短路。可以用SPFA——这个基于BFS的最短路算法变形维护。既然SPFA可以,同理Dijkstra就也可以了。【????】所以最后代码写出来是一个bfs和最短路的结合做法【感觉上哪个都不算……】
甚至是可以用最短路的堆优化……让最高点更低的路径优先……
题目未免包装的太好了……
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<queue>
#define maxn 805
using namespace std;
int read() {
int x = 0, f = 1, ch = getchar();
while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}
while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
return x * f;
}
int n, m, r, c, water, w[maxn][maxn], h[maxn][maxn], dis[maxn][maxn];
int dir[4][2] = {{0, 1}, {-1, 0}, {1, 0}, {0, -1}};//形如bfs的设定……
struct node {
int x, y, dis;//dis和dis数组含义同
node() {}
node(int xx, int yy, int dd) {x = xx, y = yy, dis = dd;}
bool operator < (const node &x) const {return x.dis < dis;}//从小到大排序【优先队列反着重定义
}now;
bool in(int x, int y) {return 0 < x && x <= n && 0 < y && y <= m;}
void dij() {
memset(dis, 0x3f, sizeof dis);
dis[r][c] = h[r][c];//dis为到点(i,j)所有路径的最高点的最矮值
priority_queue<node> q;
register int tx, ty;
q.push(node(r, c, 0)); node now;
while(q.size()) {
now = q.top(); q.pop();
for(int i = 0; i < 4; i++) {
tx = now.x + dir[i][0], ty = now.y + dir[i][1];
if(!in(tx, ty)) continue;
if(max(dis[now.x][now.y], h[tx][ty]) < dis[tx][ty]){//路径上的最高点比目前的最高点要低
dis[tx][ty] = max(dis[now.x][now.y], h[tx][ty]);//更新
if(dis[tx][ty] < water) q.push(node(tx, ty, dis[tx][ty]));
//高于初始水位线就不能更新别人了。一定是负数的值。
}
}
}
}
signed main() {
n = read(), m = read(), water = read();
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++) {
h[i][j] = read();//h是高度
}
r = read(), c = read();
dij();
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++)
printf("%d ", max(0, min(water, dis[i][j]) - h[i][j]));//注意要和0取max,避免负数
puts("");
}
return 0;
}
树的合并
传送门:入门OJ P2058
Description
话说moreD经过不懈努力,终于背完了循环整数,也终于完成了他的蛋糕大餐。
但是不幸的是,moreD得到了诅咒,受到诅咒的原因至今无人知晓。
moreD在发觉自己得到诅咒之后,决定去寻找闻名遐迩的术士CD帮忙。
话说CD最近在搞OI,遇到了一道有趣的题目:
给定两棵树,则总共有N*M种方案把这两棵树通过加一条边连成一棵树,那这N*M棵树的直径
(树的直径指的是树上的最长简单路径)
大小之和是多少呢?
CD为了考验moreD是否值得自己费心力为他除去诅咒,于是要他编程回答这个问题,但是这m
oreD早就被诅咒搞晕了头脑,就只好请你帮助他了。
Input
第一行两个正整数N,M,分别表示两棵树的大小。
接下来N-1行,每行两个正整数ai,bi,表示第一棵树上的边。
接下来M-1行,每行两个正整数ci,di,表示第二棵树上的边。
N<=10^5,M<=10^5,1<=ai,bi<=N,1<=ci,di<=M
Output
一行一个整数,表示答案。
Sample Input
4 3
1 2
2 3
2 4
1 3
2 3
Sample Output
53
Sol
一看就是树形dp的经典题。给你两棵树,可以连一条边合并起来,求所有合并方式的直径和。
两步——1.预处理出每个点可向上or向下延伸的最长长度
————2.累加答案。
所以难点就在——1.如何预处理
————————2.如何在以内累加答案。因为方案数明显是
种。
先处理第一个问题——随手画个图。
首先处理向下延伸——很好处理,设当前节点为u,可以直接设定为以u为根的子树最深的节点的深度 - u的深度。我们设这个数组为。
再者就是向上延伸——就比如上图对于点4,有两种情况:1.从点2的向上延伸的最长长度+1转移过来;2.从同为点2的子树的别的子节点向上走到点2再往下走转移过来。我们设为u向上延伸的最长长度,则对于u的子节点v,第一种情况为:
;
那么第二种情况呢?似乎不能直接用的值呢,因为如果就像点4一样,就刚好属于
所指的子树,这就不是一条合法路径了。所以我们还要维护一个变量
表示不和
走相同的u的儿子的另一条最长的向下延伸的路径。可以说是所谓的次长路径,当然在维护的时候还是有所不同的——因为不能和
走相同的u的儿子。
这样一来就可以处理第二种情况了。如果有,那么v就是
所指的路径【之一。这里多个儿子被判定为
的路径也没关系,因为既然有多次,那么
的值就和
是一样的。】,即
;否则可以直接为
。
那么第一个难点就解决完啦!对于第二个难点,同样有三种情况——取第一棵树的直径,取第二棵树的直径和各取一部分,让直径走新连上的边。这里我们可以用到这么一个不怎么容易想到但是可以靠积累的技巧——因为对于每个点来说,它的树上可以延伸的最长长度就是,也就是说对于直径某一端的节点,取出来的max就是这棵树的直径,所以可以先得到两棵树的最大直径是多少。很明显,如果对于某两个点来说,连上一条新的边走的长度还不如树内部的最大的直径,那么对答案的贡献就是这个最大的直径。所以我们对于两棵树得到的每个点的最长延伸长度a和b分别从小到大排序,则我们的内部最长直径
。从后往前考虑每一个
,从前往后考虑每一个
,让
递减,
递增,看这样组合的答案是否优于ma。而且有一个小结论——
递减,所以能让比
要大的
都必须加上才能优于ma的
,就可以毫不犹豫地把
前面的所有a的部分考虑为加上了不会比ma优,直接记为用ma替代的范围。
可能对于这个技巧,文字描述还不如代码里的那两三行好理解……所以上代码了……
代码参考:https://blog.csdn.net/liankewei123456/article/details/81905808
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#define maxn 100005
//#define int long long
using namespace std;
typedef long long ll;
int read() {
int x = 0, f = 1, ch = getchar();
while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}
while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
return x * f;
}
struct edge {
int to, nxt; edge() {}
edge(int tt, int nn) {to = tt, nxt = nn;}
}e[maxn << 1];
int head[maxn], k = 0;
void add(int u, int v) {e[k] = edge(v, head[u]); head[u] = k++;}
int n, m;
int f[maxn], g[maxn], dp[maxn];//向下的最长,次长,向上延伸的最长
ll a[maxn], b[maxn], sum[maxn], ans = 0, ma = 0;
void dfs1(int u, int fa) {
register int v, Max = -1e9, max2 = -1e9;//最长和次长
for(int i = head[u]; ~i; i = e[i].nxt) {
v = e[i].to; if(v == fa) continue;
dfs1(v, u);
if(f[v] > Max) max2 = Max, Max = f[v];//考虑每一个儿子节点,所以最长次长是不会重复对应子节点的。
else max2 = max(max2, f[v]);
}
if(Max == -1e9) f[u] = 0, g[u] = -1e9;
else f[u] = Max + 1, g[u] = max2 + 1;//存结果。+1是因为max和max2存的是相对于子节点的,到点u还要+1.
}
void dfs2(int u, int fa) {
register int v;
for(int i = head[u]; ~i; i = e[i].nxt) {
v = e[i].to; if(v == fa) continue;
dp[v] = max(dp[v], dp[u] + 1);//直接从上面过来——情况1
if(f[v] == f[u] - 1) dp[v] = max(dp[v], g[u] + 1);//从同父的别的子树过来——情况2
else dp[v] = max(dp[v], f[u] + 1);
dfs2(v, u);
}
}
signed main() {
// freopen("in.txt", "r", stdin);
memset(head, -1, sizeof head);
n = read(), m = read();
register int u, v;
for(int i = 1; i < n; i++) u = read(), v = read(), add(u, v), add(v, u);
dfs1(1, 0), dfs2(1, 0);
for(int i = 1; i <= n; i++) a[i] = max(f[i], dp[i]);
//因为可以重复利用f、g、dp数组。整合到a和b过后就没用了。
memset(e, 0, sizeof e); memset(head, -1, sizeof head); k = 0;
memset(dp, 0, sizeof dp); memset(f, 0, sizeof f); memset(g, 0, sizeof g);
for(int i = 1; i < m; i++) u = read(), v = read(), add(u, v), add(v, u);
dfs1(1, 0), dfs2(1, 0);
for(int i = 1; i <= m; i++) b[i] = max(f[i], dp[i]);
sort(a + 1, a + 1 + n); sort(b + 1, b + 1 + m);
for(int i = 1; i <= n; i++) sum[i] = sum[i - 1] + a[i];//sum存个前缀,方便用
ma = max(a[n], b[m]);
register int l = 1;
for(int i = m; i > 0; i--) {//这里枚举a或者b都可以,只是后面所有的操作都要反转一下的。
while(b[i] + a[l] + 1 < ma && l <= n) l++;//l以前的所有a[]都是和这个b[i]匹配对答案没有贡献的
ll num = n - l + 1;//对答案有贡献的部分
ans += b[i] * num + sum[n] - sum[l - 1] + num + ma * (n - num);
//ans += b[i]num次贡献 + a[l] ~ a[n]对答案的贡献 + 走num次新连上的边 + ma * 被ma替代的次数
}
printf("%lld\n", ans);
return 0;
}
完结!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!写了超级久的。
迎评:)
——End——