Codeforces Round 890 (Div. 2) supported by Constructor Institute
E1
题目链接:
题目大意:
给的一棵根为1的树,节点数为n,边数为n-1, 给定一个大小为n的排列,重新对n个节点赋值a1,a2....an,其中a1,a2……an是大小为n的排列。对于每一个节点,设节点被赋赋值为x,求节的两个子树中节点分别为yi和zi,其中(yi>x&&zi<x)或者(yi<x&&zi>x),也就是子树中存在y,z,使得lca(y,z)=x。对树重新赋值,出现最多的这样的序列对的最大个数。
数据范围:
n<=5e3
思路:
看到数据范围,发现O(n²)能过,会往dp方向思考。而对于一个节点,它的子树重新排列后的结果,对答案的贡献为:x*(siz-x-1),这里siz表示节点的树的大小,x表示比根节点小的数,那么贡献为比根节点小的数的个数*比根节点大的数的个数。也就是这个是一个二元一函数,发现其对称轴x=-b/2a为x=(siz-1)/2,在这个点上可以获得贡献的最大值。所以问题就转换:所有子树的大小来凑一个(siz-1)/2,很明显这可以用一个背包来实现,将大小看做体积,同时价值也是大小,使用滚动优化节省一维空间即可。而其它节点也是可以递归的实现。
做法:
用一个dfs预处理出所有的子树大小,然后再用一个dfs递归地跑背包,累加上对答案的最大贡献即可。
标签:
背包、动态规划,dfs、数学
Rating
1800
代码实现:
void solve() {
int n;
cin >> n;
vector<vector<int>>e(n + 1);
for (int i = 2; i <= n; ++i) { //建图
int x;
cin >> x;
e[x].push_back(i);
}
vector<int>siz(n + 1);
int res = 0;
function<void(int)>dfs1 = [&](int u)->void { //预处理出所有的子树大小
siz[u]++;
for (auto& v : e[u]) {
dfs1(v);
siz[u] += siz[v];
}
};
function<void(int)>dfs2 = [&](int u)->void {
vector<int>dp(n + 1);
int m = siz[u] - 1 >> 1;
for (auto& v : e[u]) { //跑背包
for (int i = m; i >= siz[v]; --i) {
dp[i] = MAX(dp[i], dp[i - siz[v]] + siz[v]);
}
}
int s = dp[m] * (siz[u] - 1 - dp[m]);
res += s;
for (auto& v : e[u]) { //递归下一个点
dfs2(v);
}
};
dfs1(1);
dfs2(1);
cout << res << endl;
}
时间复杂度:
O(n²)
Codeforces Round 881 (Div. 3)
D
题目链接:
题目大意:
给定一棵大小为n的树,边为n-1,给定q个询问,每次询问给出一个x,y,表示树的两个节点。两个节点可以按任意顺序往他们的叶子方向移动,当x,y同时移动到叶子节点使,得到一个二元序列(x',y'),求可以获得的二元序列最大值。
数据范围 :
2≤n≤2e5,1≤q≤2e5
思路:
容易发现,这题是一个离线查询问题,对于2e5的数据,每次查询的时间复杂度最差不能超过O(sqrt(n)),于是会想到预处理所有每个子树包含的叶子节点个数。设x子树的叶子节点个数为a,y子树的叶子结点的个数为b,那么易得所有二元组的个数为a*b。 所以这道题最重要的就是预处理出所有子树的的叶子结点个数,而这个预处理可以通过一个树形dp(递归)来实现。
做法:
先建图,写一个dfs来预处理出所有子树的叶子结点个数,然后就可以进行q次O(1)查询。
标签:
树形dp、dfs、回溯
Rating
1200
代码实现:
void solve() {
int n;
cin >> n;
vector<vector<int>>e(n + 1);
for (int i = 1; i <= n - 1; ++i) { //建图
int x, y;
cin >> x >> y;
e[x].push_back(y);
e[y].push_back(x);
}
vector<int>lef(n + 1);
function<void(int, int)>dfs = [&](int u, int fa)->void { //树形dp
int tt = 1;
for (auto& v : e[u]) {
if (fa == v) continue;
tt = 0;
dfs(v, u);
lef[u] += lef[v]; //回溯递推出所有的子树的叶子结点个数
}
if (tt == 1) lef[u] = 1; //tt=1表示当前节点为叶子节点
};
dfs(1, -1);
int q;
cin >> q;
while (q--) { //O(1)查询
int x, y;
cin >> x >> y;
cout << lef[x] * lef[y] << endl;
}
}
时间复杂度:
O(n+q)
洛谷P2602
数字计数
题目链接:
Problem - A - Codeforces[ZJOI2010] 数字计数 - 洛谷Problem - A - Codeforces
题目大意:
给定一个区间[a , b] 求区间内每个数位出现的总个数。
数据范围 :
1≤a≤b≤1e12
思路:
暴力O(n)是不可能的了。所以这题要么用dp,要么用记忆化搜索(本质上记忆化搜索也算一种dp吧)
做法:
首先对于一个数,我们需要枚举它的每一个位,这里选择从高位向低位枚举,比较方便。具体来说,对于每个位,我们需要预处理一个 dp[i]=i*pow(10,i-1),这里的i表示第i位。对于每一个数字,都有贡献i*pow(10,i-1)。然后我们需要吧数位分三类讨论,数位大于当前位,数位等于当前位,数位小于当前位。
① 数位大于当前位:贡献增加pow(10,i-1)
②数位等于当前位:贡献增加 sum(a[i]*pow(10,i-1)+a[i-1]*pow(10,i-2)....)
③数位小于当前位 没有额外贡献
注意,枚举数位小于当前位的时候不要枚举0,因为会产生额外贡献,即会产生前导0
最后,用前缀和的思想来求一下区间内的个数即可
标签:
数位dp,前缀和
Rating
普及+
代码实现:
int dp[20],num[20]; //dp[i]表示第i位的预处理结果
void init(){
//预处理dp函数,如0-9每个数位都有1个,0-99每个数位有20个,0-999每个数位有300个
for(int i=1;i<=15;++i){
dp[i]=i*pow(10LL,(int)(i-1));
}
}
void query(int x,vector<int>&cnt){
int len=0,res=0;
while(x){
num[++len]=x%10;
x/=10;
}
for(int i=len;i>=1;--i){ //从高到低枚举x的数位
for(int j=0;j<=9;++j) cnt[j]+=dp[i-1]*num[i]; //放在当前位后面,全部数都雨露均沾
for(int j=1;j<num[i];++j) cnt[j]+=pow(10LL,(int)(i-1)); //判断小于当前位置的数位
int num2=0;
for(int j=i-1;j>=1;--j) num2=num2*10+num[j]; //特判当前位置等于的数位
cnt[num[i]]+=num2+1;
}
}
void solve(){
init();
int x,y;
cin>>x>>y;
vector<int>cnt1(10),cnt2(10);
query(x-1,cnt1);
query(y,cnt2);
for(int i=0;i<=9;++i) cout<<cnt2[i]-cnt1[i]<<" ";
cout<<endl;
}
时间复杂度:
O(1) 只与数位长度有关
Codeforces Round 875 (Div. 1)
A
题目链接:
题目大意:
一颗大小为n的树,有n-1条边,按照给出的边的顺序建树,每次建边按顺序从上到下,只能找已有的点建边。问最少需要从上到下建边几次。
数据范围 :
2≤n≤2⋅1e5
思路:
这题模拟去做肯定会TLE,因为当树退化成一条链时,最差结果是O(n^2)。所以这题需要使用dp来做。由于这是一棵树,所以我们可以使用bfs或者dfs来遍历树,同时使用dp记录每个节点的最少操作。dfs的代码较少,而且思路很简单,这里故使用dfs遍历树。
做法:
dp[i]表示第i个节点所需最少操作数。考虑转移,从根节点开始,每次转移都是从根节点的子树中来转移,我们选择一个最大的来转移,因为每一个节点的操作次数取决于子节点中最大的那一个。我们需要记录一开始给出边的顺序,这里用一个map来记录,然后去跑dfs,从叶子结点开始转移上来,如果发现子节点的顺序在当前节点之前,那么就需要从头开始操作一次,此时dp[u]=dp[v]+1,否则dp[u]=dp[v]
标签:
树形dp、dfs、回溯
Rating
1400
代码实现:
void solve() {
int n;
cin>>n;
map<pii,int>mp;
vector<vector<int>>e(n+1);
for(int i=1;i<=n-1;++i){
int x,y;
cin>>x>>y;
mp[{x,y}]=i;
mp[{y,x}]=i;
e[x].push_back(y);//存建边顺序
e[y].push_back(x);
}
vector<int>dp(n+1);
function<void(int,int)>dfs=[&](int u,int fa)->void{
for(auto &v:e[u]){
if(v==fa) continue;
dfs(v,u);//一直递归到叶子结点
dp[u]=max(dp[u],dp[v]+(mp[{u,v}]<mp[{fa,u}])); //dp转移
}
};
dfs(1,-1);
cout<<dp[1]+1<<endl;//次数至少为1
}
时间复杂度:
O(nlog2n)
Codeforces Round 863 (Div. 3)
E
题目链接:
Problem - A - CodeforcesProblem - E - CodeforcesProblem - A - Codeforces
题目大意:
给定一个无限长的序列,该序列是1~inf去掉含有4的序列。给出一个k,求该序列第k个数是什么。
数据范围 :
1≤k≤1e12,1≤t≤1e4
思路:
看到这个数据范围,大概可以知道这题不是数位dp就是二分了,事实上这题是二分+数位dp,即二分答案,数位dp来check。虽然有更好的方法,但是这题的思路这样很容易就能想到。
做法:
首先我们需要预处理一下dp,计算一下 i 在 1- i 中排在第几位,这里dp[i][j]表示长度为i,最后一位是j的数字排在第几位,换一句话说,也就是1-i*10+j中去除掉4,还有多少个数。接下来,我们在二分答案的时候,使二分的答案尽量靠左,因为存在例如13,14,它们在这个序列中都排在一个位置,然而14不存在于序列中,故取最小的。
标签:
数位dp、二分。
Rating
1500
代码实现:
int dp[20][10]; //dp[i][j]表示长度为i的最后一位是j的数在序列中的位置
void init(){
for(int i=0;i<=9;i++) if(i!=4) dp[1][i]=1;
for(int i=2;i<20;i++){
for(int j=0;j<=9;j++){
if(j==4) continue; //当前位是4,跳过
for(int k=0;k<=9;k++){
if(k==4) continue; //不转移有4的方案数
dp[i][j]+=dp[i-1][k];
}
}
}
}
void solve(){
int n;
cin>>n;
auto check=[&](int x){
int num[20];
int len=0,sum=-1,lst=0;
while(x){
num[++len]=x%10;
x/=10;
}
for(int i=len;i>=1;i--){ //长度为i
for(int j=0;j<num[i];j++){ //最后一位为num[i],要加上长度为i,最后一位小于num[i]的方案数
if(j==4) continue; //不加上4
sum+=dp[i][j];
}
if(num[i]==4) break; //当前位是4,直接跳过这一位
if(i==1) sum++; //长度为1,没得转移,只有它本身
}
return sum;
};
int l=1,r=1e18,res=0;
while(l<=r){ //二分答案
int mid=l+r>>1;
int x=check(mid);
if(x>=n){ //答案靠左
res=mid;
r=mid-1;
}
else l=mid+1;
}
cout<<res<<endl;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
init(); //记得初始化
int t = 1;
cin >> t;
while (t--) {
solve();
}
return 0;
}
时间复杂度:
接近(O(t*log2(1e18)*100)),t组,100是数位dp的复杂度,log2(1e18)是二分的复杂度,总复杂度接近1e7,能过