Permutation Restoration(1900)
题目大意:设
a
1
⋯
a
n
a_1 \cdots a_n
a1⋯an是1-n的排列,给定
b
1
=
⌊
1
a
1
⌋
,
b
2
=
⌊
2
a
2
⌋
⋯
b
n
=
⌊
n
a
n
⌋
b_1 = \lfloor \frac{1}{a_1} \rfloor,b_2 = \lfloor \frac{2}{a_2 } \rfloor \cdots b_n = \lfloor \frac{n}{a_n} \rfloor
b1=⌊a11⌋,b2=⌊a22⌋⋯bn=⌊ann⌋
求任意一个满足条件满足条件
a
1
⋯
a
n
a_1 \cdots a_n
a1⋯an。其中
n
≤
5
∗
1
0
5
n \leq 5 * 10^5
n≤5∗105
思路:每个位置可二分求出需要的
a
a
a的最小值和最大值,设为区间[min,max],一共n个区间。n个区间每个区间需要从自身表示范围中找个代表,且每个区间代表不同,即区间与代表一一对应。关键是使用1-n每个数字去选择区间,而不是为每个区间选择数字。显然可以遍历1-n,贪心选择包含当前数值最早结束的区间,使用堆维护。
#include <bits/stdc++.h>
using namespace std;
int main()
{
int t;
cin >> t;
for(int i = 0;i<t;i++)
{
int n; cin >> n;
int b[n];
for(int j = 0;j<n;j++) scanf("%d",&b[j]);
vector<pair<int,int>> c[n+1];
for(int j = 1;j<=n;j++)
{
int l = 1,r = n+ 1;
while(l < r) //找最后一个
{
int m = (l + r) >> 1;
if(b[j-1]<=j/m) l = m + 1;
else r = m;
}
int a = --r;
l = 1,r = n + 1;
while(l < r)
{
int m = (l + r) >> 1;
if(b[j-1]<j/m) l = m + 1;else r = m;
}
c[r].push_back({a,j});
}
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>> q;
vector<int> ans;
for(int j = 1;j<=n;j++)
{
while(!q.empty() && q.top().first < j) q.pop();
for(auto& d:c[j]) q.push(d);
b[q.top().second - 1] = j;
q.pop();
}
for(int j = 0;j<n;j++) {printf("%d",b[j]); if(j < n - 1) printf(" ");}
printf("\n");
}
system("pause");
return 0;
}
Fixed Point Guessing(1600-交互题)
题目大意: 给定 [ 1 , 2 , 3 ⋯ n ] [1,2,3\cdots n] [1,2,3⋯n],其中n为奇数。固定其中一个数字,对其余n-1个数字配成 n − 1 2 \frac{n-1}{2} 2n−1对互相交换形成数组a。
每次查询可指定l与r(l=1是起始位置),返回
a
[
l
:
r
]
a[l:r]
a[l:r]升序排列后的结果。
n
<
=
10000
n<=10000
n<=10000,最多进行15次查询。求那个固定的数字。
思路:
随着r增大,a[1:r]先不包含固定点,再突变包含固定点。因此使用二分查找。二分r值,如何查询
a
[
1
:
r
]
a[1:r]
a[1:r]中是否包含固定点:注意交换操作。a[1:r]中可能存在[1:r]间的数字对自身交换,也可能存在与外部的交换。
- 若包含突变点,则自身交换和固定点共同构成范围[1:r]中的数字,个数为奇数个。
- 否则,则仅自身交换点构成[1:r]中的数字,个数为偶数个。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 1e9+7;
int main()
{
int t;
scanf("%d",&t);
for(int i = 0;i<t;i++)
{
int n; scanf("%d",&n); //a数组的长度
int low = 0,high = n;
while(low < high)
{
int m = (low + high) >> 1;
int o = 0;
printf("? %d %d\n",1,m+1);
fflush(stdout);
for(int j = 0;j<=m;j++)
{
int a; scanf("%d",&a);
if(a>=1 && a<=m+1) o++;
}
if(o % 2 == 0) low = m + 1; else high = m;
}
printf("! %d\n",low+1);
fflush(stdout);
}
system("pause");
return 0;
}
Placing Jinas(2000)
题目大意:
给定长宽无限二维网格,每一行从头开始是连续数量的白色格点,且连续数量随行增加而单调不增,每行其余格点为黑色。
一次操作定义为将一个玩偶(假设位于结点(i,j))移走,并在(i+1,j),(i,j+1)位置分别放置一个新玩偶。初始阶段仅(0,0)处有玩偶,求当所有白色格点均不包含玩偶时的最小操作次数。
数据范围:格点维度
n
≤
2
⋅
10
∗
5
n \leq 2 \cdot 10*5
n≤2⋅10∗5,每行连续白色格点数
a
i
≤
2
∗
1
0
5
a_i \leq 2*10^5
ai≤2∗105
不太正确的dp:
设
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]从(i,j)将该点上的一个玩偶完全移走所需最小操作数。
d
p
[
i
]
[
j
]
=
d
p
[
i
]
[
j
+
1
]
+
d
p
[
i
+
1
]
[
j
]
+
1
dp[i][j] = dp[i][j+1] + dp[i+1][j] + 1
dp[i][j]=dp[i][j+1]+dp[i+1][j]+1
数据范围限制不可以分别求dp。而边界的(i,j)都是不固定的,因此不好使用一个公式得出dp[0][0]。
正解:(如何划分解空间,分而治之)
考察每个操作如何得到,即找一一对应
注意需要总操作数实际上就是过程中途径所有格点的点的数量。而途径(i,j)格点点数目就是途径(i-1,j)格点点数和途径(i,j-1)格点点数。两者不存在交集,因为只能由(i-1,j-1)复制得到。
所以设
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j],操作序列途径(i,j)格点的点数。即
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
+
d
p
[
i
]
[
j
−
1
]
dp[i][j]=dp[i-1][j]+dp[i][j-1]
dp[i][j]=dp[i−1][j]+dp[i][j−1].且
d
p
[
0
]
[
0
]
=
1
dp[0][0]=1
dp[0][0]=1。画出递归树,i+j步到达递归终点,所有可能的路径中,只有恰好出现i次向下(即j次向右)才是递归树从根到叶子dp[0][0]的有效路径。因此
d
p
[
i
]
[
j
]
=
C
i
+
j
i
dp[i][j] = C_{i+j}^i
dp[i][j]=Ci+ji
所以最终答案为
∑
i
=
0
n
∑
j
=
0
a
i
−
1
C
i
+
j
i
\sum_{i = 0}^n \sum_{j = 0}^{a_i - 1} C_{i+j}^i
i=0∑nj=0∑ai−1Ci+ji
而
∑
j
=
0
a
i
−
1
C
i
+
j
i
=
C
i
+
a
i
i
+
1
\sum_{j = 0}^{a_i - 1} C_{i+j}^i = C_{i+a_i}^{i+1}
j=0∑ai−1Ci+ji=Ci+aii+1
上面是一个重要的组合数公式。
这是因为
C
i
+
a
i
i
+
1
=
C
i
+
a
i
−
1
i
+
1
+
C
i
+
a
i
−
1
i
C_{i+a_i}^{i+1} = C_{i+a_i - 1}^{i+1} + C_{i+a_i - 1}^{i}
Ci+aii+1=Ci+ai−1i+1+Ci+ai−1i再展开。
#include <bits/stdc++.h>
using namespace std;
const int MOD = 1e9 + 7;
class Comb {
vector<int> Facs, Invs;
void expand(size_t n) {
while(Facs.size() < n + 1) Facs.push_back(1ll * Facs.back() * Facs.size() % MOD);
if(Invs.size() < n + 1) { // 线性求阶乘的逆元
Invs.resize(n + 1, 0);
Invs.back() = 1;
for(int a = Facs[n], p = MOD - 2; p; p >>= 1, a = 1ll * a * a % MOD)
if(p & 1) Invs.back() = 1ll * Invs.back() * a % MOD; // 快速乘求 n! 的逆元
for(int j = n-1; !Invs[j]; --j) Invs[j] = 1ll * Invs[j+1] * (j + 1) % MOD;
}
}
public:
Comb() : Facs({1}), Invs({1}) {}
Comb(int n) : Facs({1}), Invs({1}) { expand(n); }
int operator() (int n, int k) {
if(k > n) return 0;
expand(n);
return (1ll * Facs[n] * Invs[n-k] % MOD) * Invs[k] % MOD;
}
};
Comb comb;
typedef long long ll;
int main()
{
int n;
scanf("%d",&n);
ll a[n+1];
for(int i= 0;i<n+1;i++) scanf("%d",&a[i]);
ll ans = 0;
for(int i = 0;i<=n;i++)
{
ans = (ans + comb(i + a[i],i+1)) % MOD;
}
cout << ans << endl;
system("pause");
return 0;
}
Split Into Two Sets(1600)
题目大意:
给定若干
[
a
i
,
b
i
]
,其中
1
≤
i
≤
n
[a_i,b_i],其中1 \leq i \leq n
[ai,bi],其中1≤i≤n,
a
i
,
b
i
a_i,b_i
ai,bi都是[1-n]间正整数。是否存在将所有
[
a
i
,
b
i
]
[a_i,b_i]
[ai,bi]划分成两个集合的方法,使得每个集合中所有数字都不相同。
其中n为偶数,且
n
≤
1
0
5
n \leq 10^5
n≤105
思路
每个数字视作结点,一个
[
a
i
,
b
i
]
[a_i,b_i]
[ai,bi]为一条边。首先必须满足每个顶点度为2,因此图就由不相交的环构成
错误做法:
任意找一条边,count++,删除这条边并删除关联两个顶点的所有边。直到删完所有边。检查count是否等于n/2
错误原因:偶数长度环也可能只选择了2条边。
正确做法:
判断所有环长是否都是偶数
#include <bits/stdc++.h>
using namespace std;
vector<bool> vis;
int main()
{
int t;
scanf("%d",&t);
for(int s = 0;s<t;s++)
{
int n;scanf("%d",&n);
int a,b;
vector<int> adj[n+1];
bool f = true;
for(int i = 0;i<n;i++)
{
scanf("%d%d",&a,&b);
adj[a].push_back({b});
adj[b].push_back({a}); //边的序号
if(a == b) {f = false;}
}
if(f) {for(int i = 1;i<=n;i++) if(adj[i].size() != 2) {f = false;break;}}
if(!f) cout << "NO" << endl;
else
{
vis.resize(n+1,false);
for(int i = 1;i<=n;i++) vis[i]=false;
bool f = true;
//cout << vis.size() << endl;
for(int i = 1;i<=n;i++)
{
if(!vis[i])
{
int cur = i;
int l = 1;
vis[cur] = true;
while(true)
{
int next = vis[adj[cur][0]]?adj[cur][1]:adj[cur][0];
if(!vis[next]) {vis[next]=true;l++;cur=next;}
else break;
}
if(l % 2!=0) f = false;
}
if(!f) break;
}
if(f) printf("YES\n"); else printf("NO\n");
// int ju = 0; //错误做法
// set<int> al;
// for(int i = 1;i<=n;i++)
// {
// while(!adj[i].empty() && al.find(adj[i][adj[i].size() - 1].second) != al.end())
// adj[i].pop_back();
// if(adj[i].empty()) continue;
// pair<int,int> q = adj[i][adj[i].size() - 1]; ju++;
// al.insert(q.second);
// for(auto y:adj[i]) al.insert(y.second);
// for(auto z:adj[q.first]) al.insert(z.second);
// adj[i].clear();
// adj[q.first].clear();
// }
// assert(al.size() == n);
// if(ju == n/2) cout << "YES" << endl;
// else cout << "NO" << endl;
}
}
system("pause");
return 0;
}
Permutation Graph(GOOD 1900)
题目大意:
给定整数1-n的排列
a
1
,
a
2
⋯
a
n
a_1,a_2 \cdots a_n
a1,a2⋯an。有一张无权无向图结点编号为1-n,结点i和结点j间有一条边当且仅当下两条件满足之一:
- m i n ( a i , a i + 1 ⋯ a j ) = a i min(a_i,a_{i+1}\cdots a_j) = a_i min(ai,ai+1⋯aj)=ai且 m a x ( a i , a i + 1 ⋯ a j ) = a j max(a_i,a_{i+1}\cdots a_j) = a_j max(ai,ai+1⋯aj)=aj
- m a x ( a i , a i + 1 ⋯ a j ) = a i max(a_i,a_{i+1}\cdots a_j) = a_i max(ai,ai+1⋯aj)=ai且 m i n ( a i , a i + 1 ⋯ a j ) = a j min(a_i,a_{i+1}\cdots a_j) = a_j min(ai,ai+1⋯aj)=aj
求结点1到结点n的最短路径长。
巧妙思路:
找特殊值(这里就是最值),找划分点.利用最短路径的最优子结构。
首先,编号相邻结点之间一定有一条有向边
其次,最短路径序列上结点编号一定是严格递增的,即不会回退(根据定义容易验证)。
最后,从1到达n的路径必经过
m
a
x
a
i
max \ a_i
max ai的结点i。因此原问题被分为两个子问题:从1-i的最短路径 + 从i-n的最短路径
为求1-i的最短路径,必经过
a
k
a_k
ak最小的结点k,其中
k
≥
1
且
k
≤
i
k \geq 1 且 k \leq i
k≥1且k≤i。由此
a
k
a_k
ak和
a
i
a_i
ai间有一条边。故为1-k的最短路径长+1.求1-k的最短路径找最大值,如此交替。
对i-n的最短路径同理。时间复杂度
O
(
n
)
O(n)
O(n)
#include <bits/stdc++.h>
using namespace std;
vector<bool> vis;
//flag用于区分是前缀还是后缀
int dfs(bool flag,int i,int j,int n,int sufminind[],int sufmaxind[],int preminind[],int premaxind[],int counter)
{
if(j<=i) return 0;
if(flag)
{
int m;
if(counter % 2 == 0)
{
m = premaxind[j];
}
else m = preminind[j];
return dfs(flag,0,m,n,sufminind,sufmaxind,preminind,premaxind,counter + 1) + 1;
}
else
{
int m;
if(counter % 2 == 0)
{
m = sufmaxind[i];
}
else m = sufminind[i];
return dfs(flag,m,n-1,n,sufminind,sufmaxind,preminind,premaxind,counter+1) + 1;
}
}
int main()
{
int t;
scanf("%d",&t);
for(int i = 0;i<t;i++)
{
int n;
scanf("%d",&n);
int a[n];
for(int k = 0;k<n;k++) scanf("%d",&a[k]);
if(n == 2) {cout << 1 << endl; continue;}
int sufminind[n],sufmaxind[n],preminind[n],premaxind[n];
sufminind[n-1]=sufmaxind[n-1]=n-1;
preminind[0]=premaxind[0]=0;
for(int i = 1;i<n;i++)
{
if(a[i]<a[preminind[i-1]]) preminind[i]=i; else preminind[i]=preminind[i-1];
if(a[i]>a[premaxind[i-1]]) premaxind[i]=i; else premaxind[i]=premaxind[i-1];
}
for(int i = n - 2;i>=0;i--)
{
if(a[i]<a[sufminind[i+1]]) sufminind[i]=i; else sufminind[i]=sufminind[i+1];
if(a[i]>a[sufmaxind[i+1]]) sufmaxind[i]=i; else sufmaxind[i]=sufmaxind[i+1];
}
int counter = 1;
int m = premaxind[n-1];
int y = dfs(true,0,m,n,sufminind,sufmaxind,preminind,premaxind,counter);
int u = dfs(false,m,n-1,n,sufminind,sufmaxind,preminind,premaxind,counter);
cout << y + u << endl;
}
system("pause");
return 0;
}
常规思路:
贪心算法:
贪心选择:每次跳到右侧能够跳到最远的点。如果所有最优解第一次都不是跳到最远的点(设为点A)则任取一最优解并考察最后一次跳到A右侧的步骤实际上可以直接使用从开始点到该点的一步替代。
优化子结构:
不会回退,显然成立。
需合适的数据结构计算每个点作为区间左端点并作为最小值(最大值),并且右端点作为最大值(最小值),满足条件的区间最大长度。
并不能从头开始枚举。因为将左端点作为最小值求时可能造成 O ( n ) O(n) O(n)遍历数组,而仅当作为最大值时才有满足条件的区间,即一次只往后更新1.因此可造成 O ( n 2 ) O(n^2) O(n2)的复杂度
Equate Multisets(1700 GOOD)
题目大意:给定两个允许含多个相同元素的集合a,b。每次可选取集合b中一个元素将其除以2(下取整),或乘以2。问是否可以将集合b转换为集合a.
思路:
b转换为a中一个元素操作序列为:0个或多个除以2的操作紧随0个或多个乘以2的操作。则b中特定元素
b
i
b_i
bi转化为a的特定元素
a
i
a_i
ai的充要条件是
b
i
b_i
bi可以仅通过除以2的操作转化为(
a
i
a_i
ai不断除以2直到结果为奇数的那个数)。
于是将集合a中每个元素先不断除以2直到结果为奇数。再试图用b取匹配这个新集合。考察两集合的最大值:
- 若相同,一定是两最大值匹配
- 若a中最大值大于b,则无法匹配(b只能除以2)
- 若a中最大值小于b,则b中最大值需除以2
#include <bits/stdc++.h>
using namespace std;
vector<bool> vis;
int main()
{
int t; cin>>t;
for(int i = 0;i<t;i++)
{
int n ; scanf("%d",&n);
multiset<int> a;
multiset<int> b;
for(int k = 0;k<n;k++)
{
int c;scanf("%d",&c);while(c%2==0) c/=2; a.insert(c);
}
for(int k = 0;k<n;k++) {int c;scanf("%d",&c);b.insert(c);}
bool f = true;
while(!b.empty())
{
int x = *b.rbegin();
int y = *a.rbegin();
if(x == y)
{
a.erase(a.find(x));
b.erase(b.find(x));
}
else if(y > x) {f = false;break;}
else //x>y
{
if(x <= 1) {f = false;break;}
b.erase(b.find(x));
b.insert(x / 2);
}
}
if(f) printf("YES\n"); else printf("NO\n");
}
}