A. Misha and Forest
题目链接
http://codeforces.com/contest/504/problem/A
题目大意
给你一个无向森林里每个结点的度数,以及每个结点相连的点的编号的亦或和。要你求出这个无向森林里的每条边。
思路
由于是无向森林,初始时一定有度数为1的点,而且度数为1的点的亦或和就是唯一的与它相连的点的编号。而删去这个度数为1的点后,整个图还是一个无向森林。
于是我们可以不断重复上述操作,类似拓扑排序,用一个队列维护当前要删除的度数为1的点,如此反复即可得到答案。
但是还有很多细节需要注意,此题真的是个FST的好题。。。
代码
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <vector>
#include <map>
#define MAXV (1<<17)
using namespace std;
typedef pair<int,int> pr;
vector<pr>sol;
map<pr,bool>mp;
int degree[MAXV],xorsum[MAXV],n,m;
int q[MAXV],h=0,t=0;
bool vis[1<<17];
int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++)
{
scanf("%d%d",°ree[i],&xorsum[i]);
if(degree[i]==1)
{
q[t++]=i;
vis[i]=true;
}
}
while(h<t)
{
int u=q[h++];
int v=xorsum[u];
if(!degree[u]) continue; //!!!!!
degree[u]--;
degree[v]--;
xorsum[v]^=u;
if(!mp.count(make_pair(u,v))&&!mp.count(make_pair(v,u))) sol.push_back(make_pair(u,v));
mp[make_pair(u,v)]=true;
if(degree[v]==1&&!vis[v])
{
q[t++]=v;
vis[v]=true;
}
}
printf("%d\n",sol.size());
for(int i=0;i<sol.size();i++)
printf("%d %d\n",sol[i].first,sol[i].second);
return 0;
}
B. Misha and Permutations Summation
题目链接
http://codeforces.com/contest/504/problem/B
题目大意
给你两个长度为n的排列a与b,定义fA为排列a的字典序编号,求编号为(fA+fB)mod n!的排列,字典序编号是从0到n-1,排列里的数字范围为[0,n-1]
思路
对于一个长度为
n
的全排列
其中 pre[x] 表示的是,全排列中在数字 x 左边,且数字大小比
我们可以从左到右扫一遍整个全排列,用一个平衡树 S 维护当前尚未被加入全排列的数字集合,在平衡树里查询数字
然后我们把排列
第一次尝试用pbds库里的平衡树做题,感觉非常爽!
代码
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/tree_policy.hpp>
#define MAXN 210000
using namespace std;
using namespace __gnu_pbds;
typedef tree<int,null_type,less<int>,rb_tree_tag,tree_order_statistics_node_update> bst;
int n,permua[MAXN],permub[MAXN],prec[MAXN]; //prec[i]=答案排列里,在第i个数字左边,但是又比第i个数字小d数字个数
bst A,B,C; //平衡树A和B里维护的是从左到右扫排列序列时,尚未被加入的数字集合
int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&permua[i]);
for(int i=0;i<n;i++) scanf("%d",&permub[i]);
for(int i=0;i<n;i++)
{
A.insert(i);
B.insert(i);
C.insert(i);
}
for(int i=0;i<n;i++)
{
int a=A.order_of_key(permua[i]); //找出ai有多少个比它小的但是在其左边又尚未被用过的数字
int b=B.order_of_key(permub[i]);
prec[i]=a+b;
A.erase(permua[i]);
B.erase(permub[i]);
}
for(int i=n-1;i>=1;i--) //对变进制数prec[]进行进位
{
prec[i-1]+=prec[i]/(n-i);
prec[i]%=(n-i);
}
prec[0]%=n;
for(int i=0;i<n;i++)
{
int tmp=*C.find_by_order(prec[i]); //在平衡树C里查询第prec[i]+1大的数,就是答案排列里第i位的数
printf("%d ",tmp);
C.erase(tmp);
}
printf("\n");
return 0;
}
C. Misha and Palindrome Degree
题目链接
http://codeforces.com/contest/504/problem/C
题目大意
给你一个字符串,定义一个区间
思路
首先我们可以把这个字符串左右两边相同的前缀和后缀去掉,这样的话,剩下的字符串里没有相同的前缀与后缀。
不妨设剩下的字符串在原串里的区间为 [L,R] 。显然 R=n−L+1 ,相同的前缀和后缀的长度均为 L−1 。在这个剩下的字符串(以下简称 S′ 串)里,所有的操作区间,要么左端点是字符串的开头,要么右端点是字符串的结尾。为了方便起见,下面我们只讨论如何统计左端点是字符串的开头的操作区间个数,而右端点是字符串的结尾的操作区间个数的统计,可以把 S′ 串翻转过来,和之前按照一模一样的做法即可统计出答案。
由于这个区间的合法性是单调的(即
[L,r]
为合法操作区间,那么
[L′,R′]
也为单调区间,
L′≤L,r≤R′
)。我们可以二分这个操作区间
[L,r]
在
S′
串里的右端点
r
的最小值,问题变成判定性问题:操作区间为
我们可以在
O(n)
时间里先预处理每种数字
i
在
1.i和n-i+1都在操作区间内,就无所谓了,先把这一对位置放到一边暂时不管
2.i在操作区间内,但是n-i+1不在操作区间内,则用一个s[n-i+1]的数字放在i位置上
3.n-i+1在操作区间内,但是i不在操作区间内,则用一个s[i]的数字放在n-i+1位置上
4.i和n-i+1均不在操作区间内,则要求s[i]必须和s[n-i+1]相同
如果在扫描过程中发现有数字不够的情况的话,就可以直接判定这个操作区间是无效的。
那么操作完了以后,对于1剩下的点对而言,显然是一定能随便找出2个相同的数字填满他们的。
实际上覆盖了
L
的操作区间的左端点是
但是答案是小于这两种操作区间个数之和的,因为两种操作区间的个数里有相同的部分,就是同时覆盖了
代码
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#define MAXN 110000
using namespace std;
typedef long long int LL;
int n,s[MAXN],L,R;
int cnt[MAXN]; //cnt[i]=数字i的出现次数
int tmpcnt[MAXN]; //tmpcnt[i]=数字i在操作区间里的出现次数
bool check(int r) //操作区间为[L,r]是否可行
{
memset(tmpcnt,0,sizeof(tmpcnt));
for(int i=L;i<=r;i++) tmpcnt[s[i]]++;
for(int i=1;i<n-i+1;i++)
{
bool LinRange=(i>=L&&i<=r); //true表明i是在操作区间内的
bool RinRange=((n-i+1)>=L&&(n-i+1)<=r); //true表明n-i+1是在操作区间内的
if(LinRange&&RinRange) continue; //i和n-i+1都在操作区间内,就无所谓了
else if(LinRange) //i在操作区间内,但是n-i+1不在操作区间内,则用一个s[n-i+1]的数字放在i位置上
{
if(--tmpcnt[s[n-i+1]]<0) return false;
}
else if(RinRange) //n-i+1在操作区间内,但是i不在操作区间内,则用一个s[i]的数字放在n-i+1位置上
{
if(--tmpcnt[s[i]]<0) return false;
}
else //i和n-i+1均不在操作区间内,则要求s[i]必须和s[n-i+1]相同
{
if(s[i]!=s[n-i+1])
return false;
}
}
return true;
}
LL calc() //假设操作区间为[l,r],l<=L,求这样的区间个数
{
int lowerBound=L,upperBound=n,ans=-1; //求出r的最小值ans
while(lowerBound<=upperBound)
{
int mid=(lowerBound+upperBound)>>1;
if(check(mid))
{
ans=mid;
upperBound=mid-1;
}
else lowerBound=mid+1;
}
return (LL)(n-ans+1)*L; //r的取值在[ans,n]范围内,l的取值在[1,L]范围内,乘法原理得到答案
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&s[i]);
L=1,R=n;
while(L<=R)
{
if(s[L]==s[R])
{
L++;
R--;
}
else break;
}
if(L>R)
{
printf("%I64d\n",(LL)n*(LL)(n-1)/2+n);
return 0;
}
int numOfOdd=0; //出现次数为奇数次的数字个数
for(int i=L;i<=R;i++)
cnt[s[i]]++;
for(int i=0;i<MAXN;i++)
if(cnt[i]&1)
numOfOdd++;
if(numOfOdd>1)
{
printf("0\n");
return 0;
}
LL ans=0;
ans+=calc();
reverse(s+1,s+n+1);
ans+=calc();
ans-=(LL)L*L;
printf("%I64d\n",ans);
return 0;
}