目录
- 第一题 Stepping Numbers
- 题意
- 思路
- 代码
- 反思
- 第二题 Nodes from the Root
- 题意
- 思路
- 代码
- 大佬的标准题解代码:
- 菜鸡我的又费空间,又费时间,又臭又长,思路又蠢的垃圾代码:
- 反思
- 第三题 Distinct Subsequences
- 题意
- 思路
- 代码
- 大佬的标准题解代码:
- 菜鸡我的又费空间,又费时间,又臭又长,思路又蠢的垃圾代码:
- 反思
- 结语
- 参考资料
今天参加了群里的一个模拟训练,训练的题目是19年的南大的题,写了三个小时,啥都没写出来,一直在写第一题,倒是有些思路,但是还是没写出来,模拟训练结束后又磨了2个小时,唉,还是放弃这道题看题解了,南大路漫漫啊~~~
给定 l , r ( 0 ≤ l ≤ r ≤ 3 e 8 ) l,r(0≤l≤r≤3e8) l,r(0≤l≤r≤3e8),问 [ l , r ] [l,r] [l,r]中的自然数满足下面条件的数有多少个。
条件:数字的任意相邻两位差值都恰好为1,且数字至少有两位。
本题 l , r l,r l,r看上去挺大,但容易观察到, 3 e 8 3e8 3e8以内满足条件的数其实很少,那怎样知道大致有多少呢?能不能暴力搜索呢?(请不要尝试将区间内所有数字都检查一遍,复杂度太高了)
若将相邻后一位减去前一位的大小记在一个数组中,则该数组应只含 1 或 -1,也就是除了第一位以外都有两种选择(没错,很像二进制!),而第一位只受区间大小限制。
这样,我们就能大致估测满足条件的数不超过 10 × 2 8 ( < 1 0 4 ) 10×2^8(<10^4) 10×28(<104),因此不会超时。很明显,能够直接暴力构造 (注意是暴力构造,而不是纯暴力)
当然暴力也有好几种,不管是一顿for循环还是不断dfs,都是高效的。
扩展:简单的采用记忆化的化就能处理 r 小 于 等 于 1 0 1 e 5 r 小于等于 10^{1e5} r小于等于101e5数量级的问题了(简称数位DP)
#include "bits/stdc++.h"
using namespace std;
int p[10];
int n,m;
int dfs(int pos,int pre,int cur){
//pos代表当前是数字中的哪一位,pre代表这一位前一位的数字,cur代表当前这一位的前缀的大小,比如1234,2的cur的大小就是1000,3就是1200.
//res存储的是在以cur前缀下,有多少Stepping Number
int result = 0;
/*递归边界:
如果递归到达了个位数后面的数字,那么进行判断,如果这个数字cur在要求范围内,则加1,否则返回0
*/
if(pos == -1){
//这个条件判断式设计的很好
if(cur >= max(n,10) && cur <= m){
result = 1;
return result;
}else{
result = 0;
return result;
}
}
//当前前缀超了m?那就进行剪枝
if(cur > m){
result = 0;
return result;
}
//如果当前的前缀的大小是0~,那么这个位置就是自定义的
if(cur == 0){
for(int i=0;i<=9;i++){
result += dfs(pos-1,i,i*p[pos]);
}
}else{
//如果当前的前缀的大小不是0~,那么说明这里不能乱放数字,要根据前一位进行判断
//后面减前面为-1的情况
if(pre > 0){
result += dfs(pos-1,pre-1,cur+(pre-1)*p[pos]);
}
//后面减前面为1的情况
if(pre < 9){
result += dfs(pos-1,pre+1,cur+(pre+1)*p[pos]);
}
}
return result;
}
int main(){
p[0] = 1;
for(int i=1;i<=10;i++){
p[i] = p[i-1] * 10;
}
int t;
scanf("%d",&t);
while(t--){
scanf("%d %d",&n,&m);
//我们这里的数据的范围是3e8,所以我们设置最高位是10e8,曲线救国,再加一个前缀0,即像这样 "0 _ _ _ _ _ _ _ _ _"
printf("%d\n",dfs(8,0,0));
}
}
include "bits/stdc++.h"
dfs()
函数的涵义:返回某前缀下所有的stepping number数,因此算是划分了子问题.
给定一棵带边权树(原题是二叉的,加强一下数据啦,考察邻接矩阵知识), n ( n ≤ 2 e 4 ) n(n≤2e4) n(n≤2e4)个节点,边权不大于 1 e 7 1e7 1e7,然后给定一个 Y ( 1 ≤ Y ≤ n ) Y(1≤Y≤n) Y(1≤Y≤n),求最小的 X ( X ≥ 0 ) X(X≥0) X(X≥0).
X X X表示边权小于 X X X的边都会被关闭, Y Y Y表示关闭这些边以后从根节点能到达的点的数量不超过 Y Y Y.
容易想到,对某一个结点,若从根结点到它的简单路径上至少有一条边被关闭,那么它就是无法到达的点。那么,如何得到这条简单路径的信息呢?
从根结点做一遍dfs就可以了!更进一步,我们想要的是最小化 X X X,因此我们先找到要关闭某个结点所需要的最小代价,这在dfs的过程中就能完成(取路径上最小的边权)。(时间复杂度为 O ( n ) O(n) O(n))
最后,只需要把这些代价存放到一个数组mi
里面,从小到大排个序(当然用sort
啦),然后直接输出mi[n-Y]+1
就好了,复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)(因为sort的时间复杂度是这个)。
#include "bits/stdc++.h"
using namespace std;
const int maxn = 2e4+7;
int n, Y;
int head[maxn], to[maxn*2], w[maxn*2], nxt[maxn*2], tot; //更常见的是用vector存边
int mi[maxn];
inline void add_edge(int u, int v, int c) {
++tot; to[tot]=v; w[tot]=c; nxt[tot]=head[u]; head[u]=tot;
++tot; to[tot]=u; w[tot]=c; nxt[tot]=head[v]; head[v]=tot;
}
void dfs(int u, int f, int m) {
mi[u]=m;
for(int i=head[u]; i; i=nxt[i]) { //采用自己喜欢的遍历方式即可
int v=to[i]; if(v==f) continue;
dfs(v,u,min(m,w[i]));
}
}
int main() {
int T; scanf("%d", &T);
while(T--) {
scanf("%d%d", &n, &Y);
for(int i=1; i<n; ++i) {
int u, v, c;
scanf("%d%d%d", &u, &v, &c);
add_edge(u+1,v+1,c); //采用自己喜欢的连边方式即可
}
dfs(1,0,1<<30);
sort(mi+1,mi+1+n);
if(n-Y==0) printf("0\n"); //这里特判一下
else printf("%d\n", mi[n-Y]+1); //+1是因为题目要求严格小于
for(int i=1; i<=n; ++i) head[i]=0; //多组数据别忘了初始化
tot=0;
}
}
#include "bits/stdc++.h"
#include
#include
#include
using namespace std;
const int maxn = 2e4+10;
struct node{
int v;
int w;
};
//用于存储输入的图的数量
int t;
//用于存储边和数量
int n,y;
//用于存储输入的边和权重
int u,v,w;
//邻接表存储一个结点连接的边和权重,需要每次初始化
vector<node> adj[maxn];
//储存每个结点的孩子的数量,不需要每次初始化
int childNum[maxn];
//存储权重的集合
set<int> st;
//存储权重的数组
int weights[maxn];
//用于表示下标是否被访问
bool vis[maxn];
int countChildNum(int root,int f){
//f表示父亲结点的编号
//初始化为1
int number = 1;
//这里面自己蕴含了递归边界!!!
for(int i=0;i<adj[root].size();i++){
if(adj[root][i].v == f){
//如果这个结点是自己的父节点,则直接下次循环
continue;
}else{
number += countChildNum(adj[root][i].v,root);
}
}
childNum[root] = number;
return number;
}
int countNodes(int x){
//输入x下,根结点所拥有的结点
//采用层序遍历进行计算
int number = childNum[0];
//每次进来都要初始化,之前因为这个一直没过题
fill(vis,vis+maxn,false);
queue<int> q;
q.push(0);
vis[0] = true;
while(!q.empty()){
int cur = q.front();
q.pop();
//设置已经访问
vis[cur] = true;
for(int i=0;i<adj[cur].size();i++){
if( vis[adj[cur][i].v] == true ){
continue;
}else if( (adj[cur][i].w) < x ){
//小于x,则进行删除
number -= childNum[adj[cur][i].v];
}else{
//入队
q.push(adj[cur][i].v);
}
}
}
return number;
}
int search(int l,int r){
//通过二分进行查找
int number = 0;
while(l != r){
int mid = (l + r) / 2;
number = countNodes(weights[mid]);
if(number > y){
l = mid + 1;
}else if(number <= y){
r = mid;
}
}
if(l==0){
return 0;
}else{
return weights[l-1]+1;
}
}
int main(){
scanf("%d",&t);
while(t--){
scanf("%d %d",&n,&y);
//注意一些共用的变量每次使用要进行初始化
for(int i=0;i<n;i++){
adj[i].clear();
}
st.clear();
//读入数据
for(int i=0;i<n-1;i++){
scanf("%d %d %d",&u,&v,&w);
node n1;
node n2;
n1.v = v;
n1.w = w;
n2.v = u;
n2.w = w;
adj[u].push_back(n1);
adj[v].push_back(n2);
st.insert(w);
}
//dfs计算每个结点的孩子结点数量
countChildNum(0,-1);
int k=0;
for(set<int>::iterator it = st.begin();it != st.end();it++){
weights[k++] = *(it);
}
//二分查找最佳的x
int ans = search(0,k-1);
printf("%d\n",ans);
}
}
大佬代码反思:
dfs()
来找到每个结点不可达的临界条件,然后后面直接使用一个数组把这些临界条件存储起来。再排个序,这样直接可以得到结果,非常快,时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n);vector
就好了);菜鸟代码反思:
u v w
中u
就是起点,v
就是终点,但是并不是这样的,题目并没有这么说,说明自己读题不仔细,想当然了;dfs
时,从root结点开始遍历目标结点就自然而然形成树了,只需要加入一些限制条件,防止结点访问它的父结点即可,你可以使用一个记录是否已访问的bool数组(需要注意的是,每次dfs之前都要进行初始化!!!),也可以把父亲结点的下标传给子节点,我写的时候太混乱了,两种都用了。
给定两个串 S , T S,T S,T ( ∣ S ∣ , ∣ T ∣ ≤ 1 e 4 |S|,|T|≤1e4 ∣S∣,∣T∣≤1e4 ) T T T串每一个字符都是随机得到的),问 S S S串中有多少个子序列等于 T T T。
要求答案对 1 e 9 + 7 1e9+7 1e9+7取模,原题其实是保证了答案不爆 i n t int int的,但由于造数据的时候很难保证答案在不爆 i n t int int的情况下还足够的强(你们懂的,数据不强容易被各种暴力做法莽过去),因此造数据的时候就造得尽可能大,但是不太清楚大家是否都了解取模的规则(离散数学里面应该学了一点的QAQ)。(我还真不知道怎么取模,我记得我离散学过呀,我记得我那时就不会。。。还有我不懂module是取模的意思。。。)
这里用到的取模知识是:
(a + b) % p = (a % p + b % p) % p (1)
(a - b) % p = (a % p - b % p) % p (2)
(a * b) % p = (a % p * b % p) % p (3)
ab % p = ((a % p)b) % p (4)
这题不能直接使用 n 2 n^2 n2进行暴力dp求解,因为评测环境1s只能运算1e8~1e9次(大部分评测环境),使用 n 2 n^2 n2的话会超时。
动态规划常见有两种用途,一种是最优化方案,另一种就是统计方案数。如果分别用一句话来描述这两种用途的特点,我会这样描述:
#include "bits/stdc++.h"
using namespace std;
const int maxn = 1e4+7;
const int mod = 1e9+7;
char s[maxn], t[maxn];
int pos[26][maxn], cnt[26]; //pos[i][j]记录字母'a'+i在T串上的所有位置(递增排列),cnt则记录数量
long long dp[maxn];
int main() {
int T; scanf("%d", &T);
while(T--) {
scanf("%s%s", s+1, t+1);
int n=strlen(s+1);
int m=strlen(t+1);
memset(cnt,0,sizeof(cnt));
for(int i=1; i<=m; ++i) dp[i]=0;
dp[0]=1;
for(int i=1; t[i]; ++i) {
int c=t[i]-'a';
cnt[c]++; //记录数量
pos[c][cnt[c]]=i; //记录位置
}
for(int i=1; s[i]; ++i) { //一位一位的枚举
int c=s[i]-'a';
for(int j=cnt[c]; j; --j) { //从后往前枚举这个字母在T串的所有位置
dp[pos[c][j]]=(dp[pos[c][j]]+dp[pos[c][j]-1])%mod;
}
}
printf("%lld\n", dp[m]);
}
}
#include "bits/stdc++.h"
#include
using namespace std;
const int maxn = 1e4 + 10;
const long long mod = 1e9+7;
const int maxm = 130;
typedef long long ll;
//s数组,这个string数组就很灵性,没说是字母数组哦
char s[maxn];
//s数组的长度
int slen;
//t数组
char t[maxn];
//t数组的长度
int tlen;
//输入测试用例数
int Q;
//dp数组
//dp[i][j]表示在s的第i位,满足后缀第j位的数量
//由于是dp,所以,只要计算当前的就好了吧
//注意数值可能很大,使用long long
ll dp[maxn];
//用于保存映射,记录每个字符在哪个位置
vector<int> mp[maxm];
int main(){
scanf("%d",&Q);
while(Q--){
//初始化dp数组
fill(dp,dp+maxn,0);
//初始化映射数组
for(int i=0;i<maxm;++i){
mp[i].clear();
}
//这一位设为1
dp[0] = 1;
//记录当前哪里为之前的
int idx = 0;
int curidx;
s[0] = 'a';
t[0] = 'a';
scanf("%s",s+1);
scanf("%s",t+1);
//居然有点忘记strlen怎么写了
slen = strlen(s) - 1;
tlen = strlen(t) - 1;
//将t的各个字符的位置读取到mp中
for(int i=1;i<=tlen;++i){
int num = t[i];
mp[num].push_back(i);
}
int cur;
for(int i=1;i<=slen;++i){
//获取当前字符
cur = s[i];
for(int j=mp[cur].size();j>0;--j){
int curpos = mp[cur][j-1];
dp[curpos] = (dp[curpos-1] + dp[curpos]) % mod ;
}
}
printf("%lld\n",dp[tlen]);
}
return 0;
}
long long
,我终于学会分析数据的大小啦;终于磨了5天把三道题写完了,撒花,这周还有,继续冲,加油(每天还要上课滴,学习不能丢~~~),我感觉这次南大的题不像我在PAT做的大多题那样直接套模板就好了,它是有一些考验智商的,比如第一题和第二题。。。我是真的没想到,只会套板子。。。
最后,树,DFS和动态规划是这次机试的考察重点!