个人思路,仅作记录,可以参考,欢迎交流。
比赛地址:传送门
A. NIT orz!
【题意】给定数组a、非负整数z和一种操作:
- 选定一个i,分别用 a[i]|z 和 a[i]&z 替换原来的 a[i] 和 z
求经过任意次操作后a中最大值的最大值
【思路】思考位运算的特点。用 a[i]|z 替换 a[i] 相当于将 a[i] 上为0的位用z相应位上的1填补;而每次操作后z将变为 a[i]&z,意味着在一次操作后,新的 a[i] 如果有为0的位,z的相应位置上也一定为0,无法再为 a[i] 填补。也就是说,z为a中每个元素的增大都最多只能贡献一次。所以只需要找到所有 a[i]|z 中的最大值即为答案。
【代码】
int a[2005];
void solve()
{
int n, z;
int ans = 0;
cin >> n >> z;
for (int i = 1; i <= n; ++i)
{
cin >> a[i];
}
for (int i = 1; i <= n; ++i)
{
a[i] |= z;
}
cout << *max_element(a + 1, a + 1 + n) << '\n';
return;
}
B. NIT Destroys the Universe、
【题意】给定数组a和一种操作:
- 将a中任意连续的若干元素都置为它们的mex值(集合中不存在的最小非负整数)
求最少需要多少次操作才能将a所有元素都变为0
【思路】一段所有元素都大于0的片段只需要一次操作就可以清零,所以答案一定小于等于上述片段数;而对任意片段进行两次操作也一定可以将其清零,所以答案一定小于等于2。综上,只要求出a中的非0片段数,再取其与2的最小值即为答案。
【代码】
int a[100005];
void solve()
{
int n;
cin >> n;
for (int i = 1; i <= n; ++i)
{
cin >> a[i];
}
bool flag = 0;
int cnt = 0;
for (int i = 1; i <= n; ++i)
{
if (flag == 0 && a[i])
{
flag = 1;
cnt++;
}
if (flag && a[i] == 0)
{
flag = 0;
}
if (cnt > 1)
{
break;
}
}
cout << min(cnt, 2) << '\n';
return;
}
C. Fishingprince Plays With Array
【题意】给定数组a,b、整数m和两种操作:
- 将a中任意一个m的倍数num展开为m个num/m
- 将a中连续m个相同的数num结合为一个num*m
求a经过任意次操作能否变为b
【思路】这两种操作互为逆操作,即这种变换是一个可逆的过程,所以只要a和b都可以变成c,a就可以变成b。分析得知:结合是在由一种情况向多种情况分岔,而展开是在由多种情况向一种情况汇总。所以由a可以得到的所有数组中有一种极端特殊情况——把所有能展开的数都展开到不可再展开——这种情况可以看作是其他所有其他情况的最高公共祖先。我们只需要把a和b都向最高祖先转化,看它们的最高祖先是否相同,就知道a是否能得到b。
【代码】
struct PAIR
{
ll num, cnt;
bool operator !=(PAIR p)
{
return num != p.num || cnt != p.cnt;
}
};
int a[50005], b[50005];
void solve()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; ++i)
{
cin >> a[i];
}
int k;
cin >> k;
for (int i = 1; i <= k; ++i)
{
cin >> b[i];
}
vector<PAIR> ar, br;
for (int i = 1; i <= n; ++i)
{
ll cnt = 1;
while (a[i] % m == 0)
{
cnt *= m;
a[i] /= m;
}
if (ar.empty() == 0 && ar.back().num == a[i])
{
ar.back().cnt += cnt;
}
else
{
ar.push_back({ a[i], cnt });
}
}
for (int i = 1; i <= k; ++i)
{
ll cnt = 1;
while (b[i] % m == 0)
{
cnt *= m;
b[i] /= m;
}
if (br.empty() == 0 && br.back().num == b[i])
{
br.back().cnt += cnt;
}
else
{
br.push_back({ b[i], cnt });
}
}
if (ar.size() == br.size())
{
for (auto i = 0; i < br.size(); ++i)
{
if (ar[i] != br[i])
{
cout << "No\n";
return;
}
}
cout << "Yes\n";
return;
}
else
{
cout << "No\n";
return;
}
}
D. Permutation Graph
【题意】给定一个长度为n的排列a,用以下方法将其转换为一个图:
以a中的每一个数作为结点。对于每一对(a[i],a[j])(i<j),如果a[i],…,a[j]中的最大值和最小值分别为a[i],a[j]或a[j],a[i],则在a[i]和a[j]之间连上一条距离为1的边。
求这张图中从a[1]到a[n]的最短距离
【思路】推理得知,从a[1]到a[n]的最短路一定是由索引低的点走向索引高的点。而在此过程中最特殊的两个点——a中所有元素的最大值点a[mx]和最小值点a[mx],值得分析。如果一个片段包含a[mx]或a[mn],则其中的最值一定有a[mx]或a[mn],也就是说a[mx]和a[mn]是必定会经过的点,不可跨越。所以所求最短路一定会包含a[mx]和a[mn]之间的这条路。而剩下的两段路可以运用分治的思想——将它们分别作为新的a’,用同样的方法排除掉a’[mx]到a’[mn]之间的路……如此递归最后就能找出a[1]到a[n]的最短路了。
在上述过程中需要频繁查找区间的最大最小值,可以建立两个st表使时间复杂度符合要求。
【代码】
int a[250005];
int stmx[250005][32];
int stmn[250005][32];
void BuildST(int n)
{
for (int i = 0; i <= log2(n); ++i)
{
for (int j = 1; j <= n; ++j)
{
if (i == 0)
{
stmx[j][i] = j;
stmn[j][i] = j;
}
else
{
stmx[j][i] = stmx[j][i - 1];
stmn[j][i] = stmn[j][i - 1];
if (j + (1 << (i - 1)) <= n)
{
if (a[stmx[j + (1 << (i - 1))][i - 1]] > a[stmx[j][i]])
{
stmx[j][i] = stmx[j + (1 << (i - 1))][i - 1];
}
if (a[stmn[j + (1 << (i - 1))][i - 1]] < a[stmn[j][i]])
{
stmn[j][i] = stmn[j + (1 << (i - 1))][i - 1];
}
}
}
}
}
return;
}
int QueryMax(int lef, int rig)
{
int len = int(log2(rig - lef + 1));
if (a[stmx[lef][len]] > a[stmx[rig - (1 << len) + 1][len]])
{
return stmx[lef][len];
}
else
{
return stmx[rig - (1 << len) + 1][len];
}
}
int QueryMin(int lef, int rig)
{
int len = int(log2(rig - lef + 1));
if (a[stmn[lef][len]] < a[stmn[rig - (1 << len) + 1][len]])
{
return stmn[lef][len];
}
else
{
return stmn[rig - (1 << len) + 1][len];
}
}
int Find(int lef, int rig)
{
if (rig - lef == 1)
{
return 0;
}
else if (rig - lef == 2)
{
return 1;
}
int maxp = QueryMax(lef, rig - 1);
int minp = QueryMin(lef, rig - 1);
return Find(lef, min(minp, maxp) + 1) + Find(max(maxp, minp), rig) + 1;
}
void solve()
{
int n;
cin >> n;
for (int i = 1; i <= n; ++i)
{
cin >> a[i];
}
BuildST(n);
cout << Find(1, n + 1) << '\n';
return;
}
E. Placing Jinas
【题意】给定非增序列a,其意义如图。初始时图中坐标系(0,0)处有一个点,每一次操作可以将(x,y)处的一个点删除并在(x+1,y)和(x,y+1)处分别放置一个新的点。求经过多少次操作可以将图中所有白色格子中的点清空。
【思路】需要对每个格子进行的操作数等于对其上方和左方格子操作数的和。若用dp需要O(Σa[i])的时间,不符要求。但观察得知每个格子需要的操作数符合组合数的规律,再由公式可以在预处理后一次性求出一排格子的操作数之和,这样就能在规定时间内完成求和。
【代码】
MOD=10**9+7
ans=0
fact=[1]
for i in range(1,4*10**5+5):
fact.append(fact[-1]*i%MOD)
def c(m,n):
return fact[m]*pow(fact[m-n]*fact[n],-1,MOD)
n=int(input())
for i,a in enumerate(map(int,input().split())):
if a>0:
ans+=c(a+i,i+1)%MOD
ans%=MOD
print(ans)
...