Sichuan University Programming Contest 2019 Preliminary

可能明年都不一定还能打校赛了吧
不知不觉都变成了一个老油条了呢
没有变的是我还是那么菜
嗯BC防AK还是不会做我好菜啊
Sichuan University Programming Contest 2019 Preliminary


A: 15块的混沌骑士

Time Limit: 1000 MS Memory Limit: 32726 K

Description

15块的混沌骑士出现了!现在给出每个混沌骑士可能出现的时间段,每个时间段可以抓捕一次混沌骑士,请问最多可以抓捕多少次混沌骑士(端点可以重合)

Input

第一行为t表示t( t &lt; 10 t &lt; 10 t<10)个测试数据,接下来每个数据开头为n( n ≤ 10000 n \leq 10000 n10000),接下来n行两个整数a,b表示一个出现时间( 0 ≤ a &lt; b &lt; 1000000000 0 \leq a &lt; b &lt; 1000000000 0a<b<1000000000)。

Output

可以抓捕多少次混沌骑士

Sample Input

1
2
1 3
3 4

Sample Output

2


可能题意写的不是特别清楚,导致很多人(我觉得很多而且包括我自己)都读错了题意
大概意思就是给你n个区间,问最多能选出多少个区间使任意两个区间都不相交(端点重合是允许的)
(出题人的意思是混沌骑士出现的时间段从头抓到尾算抓一只混沌骑士)
嗯就是贪心了
贪心策略是按结束时间排序,永远选结束时间最早的那一个(我猜也可以按开始时间排序,永远选开始时间最晚的那一个)

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e4 + 5;
struct Node {
	int l, r;
	bool operator < (const Node& node) const {
		return r < node.r || (r == node.r && l < node.l);
	}
} a[maxn];
int main() {
	int T; scanf("%d", &T);
	while(T--) {
		int n; scanf("%d", &n);
		for(int i = 1; i <= n; i++)
			scanf("%d%d", &a[i].l, &a[i].r);
		sort(a+1, a+n+1);
		int cnt = 0;
		int now = 0;
		for(int i = 1; i <= n; i++) {
			if(now <= a[i].l) {
				now = a[i].r;
				cnt++;
			}
		}
		printf("%d\n", cnt);
	}
	return 0;
}

D: 不存在的地精工程师

Time Limit: 1000 MS Memory Limit: 512M


Description

Cxh最喜欢赌狗地精,但是搜了70块都没有搜到地精工程师,因为他(地精工程师)没有解决下面这个问题,快来帮帮cxh吧。
下面是一个数列,已经给出了前三项,相信你可以知道是等差数列还是等比数列。现在给你序列的前三项,这个序列要么是等差序列,要么是等比序列,你能求出第k项的值吗。这个值对200907取模。

Input

输入:第一行一个整数 T,表示有 T 组测试数据。( T ≤ 100 T\leq100 T100
对于每组测试数据,输入前三项 a, b, c,然后输入 k。
a , b , c , k ≤ 1 e 18 a,b,c,k\leq1e18 a,b,c,k1e18
Output
对于每组数据输出第 k 项的值,对 200907取模。

Sample Input

2
1 2 3 5
1 2 4 5

Sample Output

5
16


题意很简单明了,先判断等差还是等比,然后求第k项
我是1A的,没发现什么坑点
而且后来补充了保证公比一定是整数
算等差的时候用
d = b − a d = b - a d=ba
a k = a 1 + ( k − 1 ) d a_k = a_1 + (k-1)d ak=a1+(k1)d
算等比的时候用
q = b / a q = b / a q=b/a
a k = a 1 × q k − 1 a_k = a_1 \times q^{k-1} ak=a1×qk1
算公比的时候要用到快速幂(用O(log(n))的复杂度求a的n次方,如果用O(n)的连续乘法应该会TLE)
然后我特判了一个常数列(既是等差又是等比,这样还把公比为0的情况给滤掉了(可能会RE))
嗯没什么坑点

#include<bits/stdc++.h>
using namespace std;
const int mod = 200907;
long long qpow(long long a, int n) {
	long long ans = 1;
	while(n) {
		if(n&1) ans = (ans*a)%mod;
		a = a*a%mod;
		n >>= 1;
	}
	return ans;
}
int main() {
	int T; cin>>T;
	while(T--) {
		long long a, b, c, k;
		cin>>a>>b>>c>>k;
		if(a == b && b == c) {//常数列 
			cout<<a%mod<<endl;
			continue;
		}
		if(b-a == c-b) {//等差 
			long long d = (b-a)%mod;
			long long ans = (a%mod + ((k-1)%mod*d)%mod) % mod;
			cout<<ans<<endl;
			continue;
		}
		if(b/a == c/b) {//等比 
			long long q =  b/a%mod;
			long long ans = (a%mod*qpow(q, k-1))%mod;
			cout<<ans<<endl;
			continue;
		}
	}
	return 0;
}

E: 就差一个三星月骑

Time Limit: 1000 MS Memory Limit: 512M


Description

Cxh的三星月骑就差一个了,只有做出下面这个题才能祈祷发牌员来一手神抽。
月骑住在一棵有根有权树上,接下来月骑有一些动作,动作分为两类:
1 y b,把y的权值增加b。
2 y,求y的子树上所有结点的权值之和。

Input

输入:第一行有三个整数 N,M和R根节点编号。(1<=n,m<=1e6,b<=1e6)
第二行有 N 个整数,第 i 个整数表示结点i的权值。
接下来的 N?1 行中,每行两个整数,表示一条边。
接下来的 M 行中,每行一组操作。

Output

输出:对于每组 2 y 操作,输出一个整数

Sample Input

10 14 9
12 -6 -4 -3 12 8 9 6 6 2
8 2
2 10
8 6
2 7
7 1
6 3
10 9
2 4
10 5
1 4 -1
2 2
1 7 -1
2 10
1 10 5
2 1
1 7 -5
2 5
1 1 8
2 7
1 8 8
2 2
1 5 5
2 6

Sample Output

21
34
12
12
23
31
4


嗯这题我觉得还是有一点点难度吧所以做出来还挺爽的虽然也不知道我跟标答是不是一样但是复杂度我应该是能对上的
先磨一下样例

9_6
10_2
2_-6
5_12
4_-3
7_9
1_12
8_6
6_8
3_-4

每个结点用“节点序号_权值”的形式在上图中表示出来
然后进行的操作有2种,第一种是更新一个点的权值,第二种是求一颗子树的权值之和
直接把这题写成模拟,建造一个树形,然后每个结点存其子树的权值之和,估一下复杂度的话,每次对一个结点进行更新,必然会影响到这个点到根节点的一整条链(因为其父节点以及祖先节点的权值会因为这个结点权值的改变而改变)。假设这棵树的高度为h(高度指树中离根节点最远的叶结点到根节点的距离),那么进行m次更新操作的复杂度为O(mh),而当这棵树成链状的时候,h=n-1,相当于复杂度是O(nm),肯定会过不去。就算出题人不卡,只要有分支的高度超过100,那么O(100m)就能大致卡掉(我们一般认为计算次数是O(1e8)的题目是能过的,超过1e8但是不超过1e9就看常数大小,如果超过1e9基本上都过不去,这里m是1e6)
然后我就想,这个操作和线段树的基本操作:“单点更新,区间查询”很相似。
线段树是什么这里就只简单介绍一下,这是一个基本数据结构,作用是对一个区间进行维护,支持的操作包括单点更新、区间更新、区间查询,维护的内容包括最大值、最小值、和,以及拓展出来的其他一些值。单次更新和单次查询的复杂度均为O(logn),n表示结点个数
但是线段树的单点更新和区间查询都是针对叶结点来说的,而不是对树中任意结点来说的。这怎么办呢?
我们可以把这棵树展开为一个一维数组,然后对这个一维数组维护一颗线段树
怎么展开呢??对这棵数进行一次前序DFS遍历,遍历次序就是展开次序。这样遍历有什么好处呢?这样可以保证任意一颗子树的结点在展开后一定对应一个连续的区间,而不会包括额外的其他结点。
比如上图,我就可以将树展开为(去掉了权值部分,因为用markdown画图不熟所以加上了箭头,箭头只表示从左到右的顺序关系并不代表其他)

1
2
3
4
5
6
7
8
9
10

如果给上面的结点从左到右从1到10分别分配一个下标(即9的下标为1,10的下标为2,…,3的下标为10),那么我们就可以通过一个区间找到一个子树。比方说要找结点2的子树,就只需要找区间[4,10];要找结点7的子树,就只需要找区间[6, 7]
这样将树展开之后,单点更新就只用更新这个点所对应的位置的值,而子树查询就变成了这棵子树对应的区间查询
那么怎么找到每个结点的子树对应的区间呢?之前说了,树的建立是按前序DFS遍历而得到的,我们只需要做一个计数器,记录当前遍历过的结点个数。DFS到一个结点时,将计数器的值赋值给左端点,而当这个结点对应的子树被遍历完,这次DFS准备return时,将计数器的值赋值给右端点即可。
emmmmm思路大致就这样了我也不知道怎么才能讲的更清楚了所以直接扔代码吧

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e6 + 5;
vector<int> G[maxn];
bool vis[maxn];
int idx, l[maxn], r[maxn];
long long val[maxn];
void dfs(int u) {
	if(vis[u]) return;
	vis[u] = 1; l[u] = idx++;
	for(int v : G[u]) dfs(v);
	r[u] = idx-1;
}
long long dat[maxn<<2];
int k, ql, qr;
long long v;
void update(int o, int L, int R) {
	if(L == R) dat[o] += v;
	else {
		int mid = L+R >> 1;
		if(k <= mid) update(o<<1, L, mid);
		else update(o<<1|1, mid+1, R);
		dat[o] = dat[o<<1] + dat[o<<1|1];
	}
}
long long query(int o, int L, int R) {
	if(ql <= L && R <= qr) return dat[o];
	if(qr < L || R < ql) return 0;
	int mid = L+R >> 1;
	long long s = 0;
	s += query(o<<1, L, mid);
	s += query(o<<1|1, mid+1, R);
	return s;
}
int main() {
	int n, m, root; scanf("%d%d%d", &n, &m, &root);
	for(int i = 1; i <= n; i++) scanf("%lld", &val[i]);
	for(int i = 1; i <= n; i++) G[i].clear();
	for(int i = 1; i < n; i++) {
		int l, r; scanf("%d%d", &l, &r);
		G[l].push_back(r);
		G[r].push_back(l);
	}

	memset(vis, 0, sizeof(vis));
	idx = 1; dfs(root);

	memset(dat, 0, sizeof(dat));
	for(int i = 1; i <= n; i++) {
		k = l[i]; v = val[i];
		update(1, 1, n);
	}

	while(m--) {
		int ord, y;
		long long b;
		for(int i = 0; i < 2; i++) {
			scanf("%d%d", &ord, &y);
			if(ord == 1) {
				scanf("%lld", &b);
				k = l[y]; v = b; update(1, 1, n);
			}
			else {
				ql = l[y]; qr = r[y];
				printf("%lld\n", query(1, 1, n));
			}
		}
	}
	return 0;
}

F: 锁子甲巨魔蘸酱

Time Limit: 1000 MS Memory Limit: 512M


Description

Cxh把锁子甲给了巨魔蘸酱,现在他想取下锁子甲需要解决下面这个问题。
巨魔蘸酱的城市里有n个车站,m 条双向公路连接其中的某些车站。每两个车站最多用一条公路连接,从任何一个车站出发都可以到达其他车站,巨魔蘸酱住在车站1,他有五个巨魔亲戚,需要拜访每个亲戚(顺序任意),从他们那拿到锁子甲的配件。求cxh最少行走时间。

Input

输入:第一行:n,m 为车站数目和公路的数目。(1<=n<=5e4,1<=m<=1e5)
第二行为五个亲戚所在车站编号。
以下m行,每行三个整数为公路连接的两个车站编号和时间。

Output

输出:输出仅一行,包含一个整数 T,为最少的总时间。

Sample Input

6 6
2 3 4 5 6
1 2 8
2 3 3
3 4 4
4 5 5
5 6 2
1 6 7

Sample Output

21


这题我用BFS做的emmmm我觉得标答应该会比我的方法好(也不一定)
如果要拜访的亲戚只有一个,那用BFS应该是一件十分显然的事情。但是如果要拜访两个亲戚呢?或者像题目中一样有5个亲戚呢?
这个时候我们就要在BFS的时候存入一个已经拜访的亲戚的状态标记。如果访问一个结点时这个结点已经以同样的状态被访问过,那么我们就跳过这次访问。
那么怎么标记访问的状态呢?我们可以把要拜访的5个位置的状态分别设为1, 2, 4, 8, 16,也就是1<<0, 1<<1, 1<<2, 1<<3, 1<<4(<<表示位运算左移),而其他结点的状态设为0。这样每次访问一个结点时,我们就把自己的状态与那个结点的状态作或运算。访问了5个位置之后,当前的状态应该会变成31,即二进制的11111
显然,我们总共状态的种类数有32种(00000, 00001, 00010, 00011, …, 11111)每种状态下最多能把所有结点都访问一次(因为BFS时以同样的状态到达同一个结点时可以跳过这次访问),也就是说复杂度为O(32n),理论可行
最后,BFS要注意的是,我用了priority_queue而不是queue,因为我们BFS要求的是最近的距离,所以每次要从当前最近的结点开始走。(我不知道用queue能不能行,感觉好像不行)
嗯没开long long炸了一发(虽然数据范围没超long long,但是进行加减乘除的运算过程可能会炸int,所以还是要开long long)

#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e4 + 5;
bool vis[maxn][40];
int status[maxn];
int n, m;
struct Edge {
	int v;
	long long c;
	Edge(int v, long long c) { this->v = v; this->c = c; }
};
struct Node {
	int plc, sta;
	long long step;
	Node(int plc, int sta, long long step) : plc(plc), sta(sta), step(step) {}
	bool operator < (const Node& node) const {
		return step > node.step;
	}
};
vector<Edge> G[maxn];
priority_queue<Node> q;//距离,状态 
int main() {
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; i++) G[i].clear();
	memset(status, 0, sizeof(status));
	for(int i = 0; i < 5; i++) {
		int t; scanf("%d", &t);
		status[t] = 1<<i;
	}
	
	while(m--) {
		int u, v;
		long long c; scanf("%d%d%lld", &u, &v, &c);
		if(u == v) continue;
		G[u].push_back(Edge(v, c));
		G[v].push_back(Edge(u, c));
	}
	
	while(!q.empty()) q.pop();
	q.push(Node(1, status[1], 0));
	long long ans = 0x7fffffffffffffffll;
	while(!q.empty()) {
		Node nodeU = q.top(); q.pop();
		int plc = nodeU.plc, sta = nodeU.sta;
		long long step = nodeU.step;
//		printf("plc = %d, sta = %d, step = %lld\n", plc, sta, step);
		if(vis[plc][sta]) continue;
		if(sta == 31) {
			ans = min(ans, step);
			continue;
		}
		vis[plc][sta] = 1;
		for(Edge edgeV : G[plc]) {
			int v = edgeV.v, c = edgeV.c;
			Node nodeV = Node(v, sta|status[v], step+c);
			q.push(nodeV);
		}
	}
	printf("%lld\n", ans);
	return 0;
}

G: SCUACM

Time Limit: 1000 MS Memory Limit: 512M


Description

我是一道签到题,关注SCUACM公众号,找到"ACM"一栏中"什么是ACM"一文中推荐的第四个OJ,输出其网址

Input

Output

输出一行,为推荐的第四个OJ网址


#include<bits/stdc++.h>
using namespace std;
int main() {
	printf("http://acm.scu.edu.cn/");
	return 0;
}

H: rzy的小迷弟

Time Limit: 1000 MS Memory Limit: 512M


Description

金牌教练rzy又开课了!他的小迷弟们听到这个消息蜂拥而至赶到会场。但是他的迷弟们相互之间饱含敌意,都希望与其他人离的尽可能远,好独享rzy的谆谆教诲。现在给出你会场内的座位分布,负责分配座位的cxh的任务是使任意两个小迷弟之间的最小距离尽可能的大,那你能不能帮帮cxh,计算出这个最大的最小距离是多少呢?

Input

第一行是表示座位数和人数的两个整数 n 和 m;
第二行为 n 个整数,表示会场内每个座位的位置 x i x_i xi

Output

输出仅一个整数,表示迷弟们之间最大的最小距离值。

Sample Input

5 3
1 2 10 6 5

Sample Output

4

hint

2 ≤ n ≤ 1 0 5 , 0 ≤ x i ≤ 1 0 9 , 2 ≤ m ≤ n 2 \leq n \leq 10^5, 0 \leq x_i \leq 10^9, 2 \leq m \leq n 2n105,0xi109,2mn


嗯判断最小的最大(或者最大的最小)的一种基本方法就是二分。把“求最大的最小距离”转化为判断“最小距离为d是否可行”来处理
如果给定最小距离d,怎么判断是否可行呢?我们只需要把座位的位置排序,贪心的判断每个座位离上一个人的距离是否大于等于d,如果满足就在这个座位上再坐一个人,否则就判断下一个座位。如果坐了m个人,那么就说明可以坐得下,否则就坐不下。
知道怎么判断距离d之后,怎么求最大的最小距离呢?也就是说,怎么求一个最大的d使得判断结果为真呢?显然我们的d一定是在区间 [ 0 , m a x ( x i ) ] [0, max(x_i)] [0,max(xi)]中的,我们首先让L赋值为左端点,R赋值为右端点,每次取区间中点mid判断,如果判断结果为真,说明最短距离为mid时是可以的,那么就说明我们的最大距离一定落在 [ m i d , R ] [mid, R] [mid,R]这个区间内,再将mid赋值给L,如果判断结果为假,说明最短距离为mid时是不可以的,那么就说明我们的最大距离一定落在 [ L , m i d − 1 ] [L, mid-1] [L,mid1]这个区间内,再将mid-1赋值给R
不断重复这个过程,直到L==R就得到我们的答案

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5;
int a[maxn];
int n, m;
bool judge(int k) {
	int res = m, now = -1;
	for(int i = 1; i <= n; i++) {
		if(now == -1 || a[i] - now >= k) {
			now = a[i]; res--;
			if(res <= 0) return true;
		}
	}
	return false;
}
int main() {
	scanf("%d%d", &n, &m);
	int l = 0, r;
	for(int i = 1; i <= n; i++) {
		scanf("%d", &a[i]);
		r = max(r, a[i]);
	}
	sort(a+1, a+n+1);
	while(l < r) {
		int mid = l+r+1 >> 1;
		if(judge(mid))
			l = mid;
		else r = mid-1;
	}
	printf("%d\n", l);
	return 0;
}

I: 代码查重

Time Limit: 1000 MS Memory Limit: 512M


Description

赛后cry开始了代码的查重工作,我们把代码抽象成只包含数字的字符串(长度不超过10),cry把大家写的代码按题目分类放在一起,对于某一道题,如果收集到不同同学的代码中,其中一份代码是另一份的前缀,我们就认为该题出现了代码重复情况。如果某题的代码没有出现重复,我们就认为该题所有人都通过,输出"YES",否则需要进一步的处理,输出"NO"

Input

第一行一个整数 T T T,表示需要查重的题数
对于每道题,第一行输入一个整数 n n n,表示有 n n n份不同的代码,接下来 n n n行输入 n n n份代码

Output

通过查重,输出"YES",否则输出"NO"

Sample Input

2
3
9759
97625999
979
5
51243
53421
12345
5342
54213

Sample Output

YES
NO

Hint

T <= 40, N <= 10^4


嗯判断一个字符串是否是另一个字符串的前缀,数据结构上很容易想到字典树Trie
但是因为保证数字字符串长度不超过10,这个东西是可以直接用int存下来的。不知道这里是为了诱导你用int呢还是说有更好的处理办法。感觉用int会在某些地方方便一些,比如说判断一个n位数是否是一个m位数的前缀( n ≤ m n\leq m nm,无前导零时),只需要让 m / = 1 0 m − n m/=10^{m-n} m/=10mn,再判断 n = = m n==m n==m是否成立即可。有前导零也只需要特判一下,比较这两个数之前先比较前导零的个数。但是这是个 O ( n 2 ) O(n^2) O(n2)的算法,对于1e4的数据理论能过,但是再算上额外的40组T就有点难了,应该是能卡掉的。所以也没敢写这种方法
然后还一种是比赛脑抽没想到的看起来比我的方法要简单的一种方法,就是直接进行字符串排序,然后直接比较相邻的字符串是否满足前缀关系就可以了。嗯不知道为什么反正我看到这题第一感觉就是trie就没想字符串排序了。这种方法的合理性显然吧。估复杂度的话,他需要一个O(nlogn)的排序,排序的单次比较需要一个O(L)(L表示字符串长度),算上T组数据就是O(TLnlogn),然后T=40,L=10,n=1e4,算出来大致是440101e4log(10)=O(1e7),应该属于一个写得出就能过的复杂度,然后我的方法应该是O(TLn)的,比他少了一个logn(嗯做复杂了但是还是要给自己找一个理由嘻嘻嘻)
然后就是Trie,字典树是一个存放多个字符串的数据结构,它能以比较好的空间效率存放多个字符串(虽然这一般不是我们care的),然后支持两种操作:向字典树中插入一个字符串,查询一个字符串在这棵字典树中的一些性质,比如说查询这个字符串是否在这棵字典树中,或者说这个字符串是字典树中第几个字符串等。
在这里我们要查询的就是字符串是否是字典树中某个单词的前缀,或者字符串是否完整包含有字典树中的某个单词(既有可能s是t的前缀,也有可能t是s的前缀)
下图是一个由5个单词组成的字典树(apple, apache, bless, blue, blueabc),其中结点的数字表示结点的编号(root编号为0),边上的字母表示结点沿这个字母对应的方向(对应的边),*5*表示5号结点是一个单词的结束部分

a
b
l
e
s
s
u
e
a
b
c
p
p
a
c
h
e
l
e
1
2
3
4
6
7
8
10
11
12
14
15
17
18
19
root
*13*
*16*
*9*
*5*
  • 我们在插入的时候,对于每个结点都判断一下它是不是一个单词的结束,如果是,表示当前插入的字符串已经完全包含有另一个字符串了(比如上图,如果插入blueabc的时候已经插入过blue这个单词,那么在插入过程到达13号结点的时候就知道当前字符串已经包含了blue这个单词作为前缀)。
  • 对于插入单词的最后一个字符,判断这个字符对应的结点是否已经存在,如果存在,说明当前插入的字符串是字典中另一个字符串的前缀(如上图,插入blue之前已经插入过blueabc,那么在插入最后一个字母e的时候,会发现e对应的已经存在了一个13号结点,这就说明blue是已经存在过的一个单词的前缀)。
  • 否则,说明不存在前缀关系。

我们用val[i]标记这个结点是否是一个单词的结束,用ch[u][v]表示结点u的第v条边指向的结点id,默认指向0(表示不存在这个结点)

#include<bits/stdc++.h>
using namespace std;
const int maxnode = 1e5 + 5;
const int sigma_size = 10;
struct Trie {
	int ch[maxnode][sigma_size];
	int val[maxnode];
	int sz;
	Trie() { sz = 1; memset(ch[0], 0, sizeof(ch[0])); memset(val, 0, sizeof(val)); }
	void init() { sz = 1; memset(ch[0], 0, sizeof(ch[0])); memset(val, 0, sizeof(val)); }
	int idx(char c) { return c - '0'; }
	//插入字符串s,附加信息为v,注意v必须非零,因为0表示"本节点不是单词结点" 
	bool insert(char *s) {
		int u = 0, n = strlen(s);
		for(int i = 0; i < n-1; i++) {
			int c = idx(s[i]);
			if(!ch[u][c]) {
				memset(ch[sz], 0, sizeof(ch[sz]));
				val[sz] = 0;
				ch[u][c] = sz++;
			}
			u = ch[u][c];
			if(val[u]) return false;
		}
		int c = idx(s[n-1]);
		if(ch[u][c]) return false;
		memset(ch[sz], 0, sizeof(ch[sz]));
		val[sz] = 1;
		u = ch[u][c] = sz++;
		return true;
	}
} tr;
char s[100];
int main() {
	int T; scanf("%d", &T);
	while(T--) {
		tr.init();
		int n; scanf("%d", &n);
		bool flag = true;
		while(n--) {
			scanf("%s", s);
			flag = flag && tr.insert(s);
		}
		printf(flag ? "YES\n" : "NO\n");
	}
	return 0;
}

J: 拼图游戏

Time Limit: 1000 MS Memory Limit: 512M


Description

cxh很喜欢拼拼图,他新买了一种数字拼图,含有写着 0 − 9 0-9 09的十种拼图。他最近在想,假设他想用这些拼图同时拼出 [ a , b ] [a, b] [a,b]中的所有整数,十种拼图各出现了多少次。

Input

第一行输入两个整数 a , b a, b a,b,表示cxh想同时拼成的整数范围

Output

包含一行 10 10 10个整数,分别表示需要 0 − 9 0-9 09十种拼图各多少块 。

Sample Input

0 20

Sample Output

3 12 3 2 2 2 2 2 2 2

hint

0 ≤ a ≤ b ≤ 1 0 12 0 \leq a \leq b \leq 10^{12} 0ab1012


(没禁外网吧emmmm所以在网上临时学了个奇短的代码感觉好厉害啊)
(很可能我讲不清楚所以还是自己琢磨吧,但是我还是会认真写的)
(其实这题数位dp应该能做但是我不会啊所以做到数位dp就开始想念队友)

写完我觉得是个人都看不懂(应该还有的地方写错了),所以还是直接跳过这段吧我觉得。

思路讲不清楚了。。直接讲代码吧。。
getAns是一个递归函数,getAns(x, cnt, 1)求的是从1到x这个区间内,每个数字出现的次数,并将结果储存到cnt数组里面
由于这个函数不处理0,而题目要求我们从0开始处理,所以0我们特判即可
为了方便,以下的解释中,我们做一个符号约定:我们用“abcd”来表示这个数的个位数为d,十位数为c,以此类推。用?来表示我们讨论这个位置可以放1-9任意一个数的情况,用*来表示我们讨论这个位置可以放0-9任意一个数的情况
然后将一个数x进行分类讨论,为了比方说x=6543这种情况,即求[1, 6543]区间内每个数字出现的次数
这个区间可以分为以下几类:

  • [6540, 6543]
  • [1, 653*](其实是[1, 6539],写成*是包括了6530到6539的9个数)

然后把第二个区间又可以递归的分为

  • [650*, 653*](其实是[6500, 6529])
  • [1, 64**](其实是[1, 6499])

然后第二个区间又可以进行递归的划分

简单的说,我们把x分成两种情况来讨论:

  • 固定前面的数,只讨论从最后一个不为*的数开始的后半部分(上面的第一种情况)
  • 将前半部分的个位数-1,将原来的个位数合并到*中并递归的讨论

即x=“abcd**”可以分为两个区间:

  • [abc0**, abcd**]
  • [1, ab(c-1)***]

然后[1, ab(c-1)***]又可以递归的被分为

  • [ab0***, ab(c-1)***]
  • [1, a(b-1)****]

那么也容易猜到,参数中的t表示的就是递归时尾部的一串*的组合种类数,即 1 0 k 10^k 10k(每次递归,t的值变为10倍,k表示尾部的*的长度),刚开始没有省略的时候只有1种组合
getAns主要分为四部分,前三部分讨论第一种情况,最后一部分递归地讨论第二种情况

for(int i = 0; i < 10; ++i) cnt[i] += t*x;

这一部分讨论最高位的*代表的字符出现的次数,我们约定*可以表示0-9的任意数字,所以for循环从0到9
比如123***,我们现在只统计3后面的那一个*,而之后的*应该要在上一次递归时被统计过了
这部分讨论的出现次数,是在只改变前半部分的基础上得到的,为什么呢?因为我们接下来马上就要讨论*部分变化时对前面几位的影响,所以现在讨论的这一个*被后半部分影响的计数已经在之前做过了
那么显然,*前面的数就是我们传入的x,所以每个数字都能够出现x次

for(int i = 1; i <= p; ++i) cnt[i] += t;

这部分是讨论x的前半部分的个位数的字符出现的次数,显然在固定之前几位时,每个个位数只能取决于后面*的部分的变化种数,即t次。故对每一个出现的个位数都加上t次

while(q) {
	cnt[q%10] += (p+1)*t;
	q /= 10;
}

这部分是讨论x的除个位数以外的前半部分中所有字符的出现次数,在这一部分保持不变时,每一位数都只能取决于个位数和*部分的变化,即每位数都出现了(p+1)*t次

getAns(x-1, cnt, t*10);

这部分是递归地讨论第二种情况

#include<bits/stdc++.h>
using namespace std;
long long cnt1[10], cnt2[10];
void getAns(long long x, long long* cnt, long long t) {
	if(x < 0) return;
	long long p = x%10;
	long long q = x = x/10;
	for(int i = 0; i < 10; ++i) cnt[i] += t*x;
	for(int i = 1; i <= p; ++i) cnt[i] += t;
	while(q) {
		cnt[q%10] += (p+1)*t;
		q /= 10;
	}
	getAns(x-1, cnt, t*10);
}
int main() {
	long long a, b; scanf("%lld%lld", &a, &b);
	memset(cnt1, 0, sizeof(cnt1));
	memset(cnt2, 0, sizeof(cnt2));
	if(a) cnt1[0]++; cnt2[0]++;
	getAns(a-1, cnt1, 1);
	getAns(b, cnt2, 1);
	for(int i = 0; i < 10; ++i) {
		printf("%lld", cnt2[i] - cnt1[i]);
		if(i < 9) printf(" ");
		else printf("\n");
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值