计算几何
3.1 题意描述
花花对计算几何有着浓厚的兴趣。他经常对着平面直角坐标系发呆,思考一些有趣的问
题。今天,他想到了一个十分有意思的题目:
首先,花花会在x 轴正半轴和y 轴正半轴分别挑选n 个点。随后,他将x 轴的点与y 轴
的点一一连接,形成n 条线段,并保证任意两条线段不相交。花花确定这种连接方式有且仅
有一种。最后,花花会给出m 个询问。对于每个询问,将会给定一个点P(xp; yp),问线段
OP(O 为坐标原点)与n 条线段会产生多少个交点?
3.2 输入格式
第1 行包含一个正整数n,表示线段的数量;
第2 行包含n 个正整数,表示花花在x 轴选取的点的横坐标;
第3 行包含n 个正整数,表示花花在y 轴选取的点的纵坐标;
第4 行包含一个正整数m,表示询问数量;
随后m 行,每行包含两个正整数xp 和yp,表示询问中给定的点的横、纵坐标。
3.3 输出格式
共m 行,每行包含一个非负整数,表示你对这条询问给出的答案。
3.4 样例输入
3
4 5 3
3 5 4
2
1 1
3 3
3.5 样例输出
0
3
3.6 样例解释
3条线段分别为:(3; 0) �� (0; 3)、(4; 0) �� (0; 4)、(5; 0) �� (0; 5)
(0; 0) �� (1; 1) 不与他们有交点,答案为0。
2
NOIP 模拟Day1 3. 计算几何
(0; 0) �� (3; 3) 与三条线段均有交点,答案为3。
3.7 数据规模与约定
• 对于40% 的数据:n;m 10;
• 另有20% 的数据:n;m 100;
• 另有20% 的数据:n;m 1000;
• 对于100% 的数据:n;m 105; 1 x; y < 231。
【解题报告】
由观察不难得出,对于每一个询问,OP只和在P左下角的线段相交。故我们可以二分
在P左下角的最靠上的线段,从而得到答案。
时间复杂度:O(nlogn + qlogn),期望得分100 分。
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
#define N 100010
int n,m;
long long x[N],y[N];
long long p_x,p_y;
int main()
{
freopen("geometry.in","r",stdin);
freopen("geometry.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;++i) scanf("%lld",&x[i]);
for(int i=1;i<=n;++i) scanf("%lld",&y[i]);
sort(x+1,x+n+1);sort(y+1,y+n+1);
for(scanf("%d",&m);m;--m)
{
scanf("%lld%lld",&p_x,&p_y);
int l=1,r=n;
while(l<=r)
{
int mid=(l+r)>>1;
if(x[mid]*p_y+y[mid]*p_x>=x[mid]*y[mid]) l=mid+1;
else r=mid-1;
}
printf("%d\n",r);
}
return 0;
}
4 花花的聚会
4.1 题意描述
花花住在H 国。H 国有n 个城市,其中1 号城市为其首都。城市间有n �� 1 条单向道
路。从任意一个城市出发,都可以沿着这些单向道路一路走到首都。事实上,从任何一个城市
走到首都的路径是唯一的。
过路并不是免费的。想要通过某一条道路,你必须使用一次过路券。H 国一共有m 种过
路券,每张过路券以三个整数表示:v k w:你可以在城市v 以价格w 买到一张过路券。这
张券可以使用k 次。这意味着,拿着这张券通过了k 条道路之后,这张券就不能再使用了。
请注意你同一时间最多只能拥有最多一张过路券。但你可以随时撕掉手中已有的过路券,
并且在所在的城市再买一张。
花花家在首都。他有q 位朋友,他希望把这些朋友们都邀请到他家做客。所以他想要知道
每位朋友要花多少路费。他的朋友们都很聪明,永远都会选择一条花费最少的方式到达首都。
花花需要准备晚餐去了,所以他没有时间亲自计算出朋友们将要花费的路费。你可以帮帮
他么?
4.2 输入格式
输入的第一行包含两个空格隔开的整数n 和m,表示H 国的城市数量和过路券的种数。
之后的n �� 1 行各自包含两个数ai 和bi,代表城市ai 到城市bi 间有一条单向道路。
之后的m 行每行包括三个整数vi; ki 和wi,表示一种过路券。
下一行包含一个整数q,表示花花朋友的数量。
之后的q 行各自包含一个整数,表示花花朋友的所在城市。
4.3 输出格式
输出共q 行,每一行代表一位朋友的路费。
4.4 样例输入
7 7
3 1
2 1
7 6
6 3
5 3
4 3
7 2 3
7 1 1
2 3 5
3 6 2
4 2 4
5 3 10
6 1 20
3
5
6
7
4.5 样例输出
10
22
5
4.6 样例解释
对于第一位朋友,他在5 号城市只能购买一种过路券,花费10 元并且可以使用3 次。这
足够他走到首都,因此总花费是10 元。
对于第二位朋友,他在6 号城市只能购买20 元的过路券,并且只能使用一次。之后,他
可以在3 号城市购买2 元,可以使用3 次的过路券走到首都。总花费是22 元。
对于第三位朋友,他在7 号城市可以购买两种过路券。他可以花3 元买一张可以使用2
次的券,然后在3 号城市再买一张2 元,可以使用3 次的券,走到首都。总花费是5 元,而且
其他的购买方式不会比这种更省钱。
4.7 数据规模与约定
•对于40% 的数据:n; m; q 10;wi 10;
• 另有20% 的数据:n; m; q 500;wi 100;
• 另有20% 的数据:n; m; q 5000;wi 1000;
• 对于100% 的数据:n; m; q 105;wi 10000; 1 vi; ki n。
【解题报告】
正解感觉有点复杂:
考虑优化解法三的DP转移。事实上我们只需在较快的时间内求得树上某一段区间的
minff[u]g,即可较好地解决这个问题。而维护树上区间最值有树链剖分、动态树等许多数据
结构能够胜任。在这里我们考虑用倍增的思想,维护u 往上跳2i 步到的祖先v 和u 到v 这一
段f 值的最小值,这样我们对于每一个节点u 可以在O(logn) 的时间内求得minff[v]g。
时间复杂度为O(nlogn),期望得分100 分。
然而我是用记忆化搜索水的,跑的还是坠快的
代码感觉还比较好理解
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
#define N 100010
#define inf 0x3f3f3f3f
int n,m,q,u,v,l;
int fa[N],dp[N];
int cnt=0,head[N];
struct Edge{int to,nxt,l;}e[N];
inline void adde(int u,int v,int l)
{
e[++cnt].to=v;
e[cnt].l=l;
e[cnt].nxt=head[u];
head[u]=cnt;
}
int dfs(int u)
{
if(u==1) return 0;
if(dp[u]!=-1) return dp[u];
dp[u]=inf;
for(int i=head[u];~i;i=e[i].nxt)
{
int k=e[i].to,f=fa[u],num=1;
while(f>=1&&num<=k)
{
dp[u]=min(dp[u],dfs(f)+e[i].l);
f=fa[f];
++num;
}
}
return dp[u];
}
int main()
{
freopen("party.in","r",stdin);
freopen("party.out","w",stdout);
memset(dp,-1,sizeof(dp));
memset(head,-1,sizeof(head));
scanf("%d%d",&n,&m);
for(int i=1;i<n;++i)
{
scanf("%d",&u);
scanf("%d",&fa[u]);
}
for(int i=1;i<=m;++i)
{
scanf("%d%d%d",&u,&v,&l);
adde(u,v,l);
}
for(scanf("%d",&q);q;--q)
{
scanf("%d",&u);
printf("%d\n",dfs(u));
}
return 0;
}
5 文本编辑器
5.1 题意描述
九发明了一个完美的文本编辑器。这个编辑器拥有两个光标(cursor),所以九能够同时
在两处地方插入和删除文本。这个编辑器除了正常的编辑功能以外,还有一些只有九才知道用
处的功能,例如翻转两个光标之间的文本。某一天,九把自己的完美文本编辑器给弄丢了,但
是她还有好多好多文本需要处理。于是她想请聪明又智慧的你帮她实现完美文本编辑器的一
些功能。
5.2 输入格式
第一行是初始时文本编辑器内容。
第二行是一个正整数N,N 表示操作次数。
接下来有N 行,每行有一个命令,命令格式如上方表格。
5.3 输出格式
对于每个命令,按上方表格要求执行并输出。
5.4 样例输入
goodykc
11
I R u
I R l
> L
> L
> L
> L
R
D R
< R
D R
S
5.5 样例输出
T
T
T
T
T
T
T
F
T
T
goodluck
5.6 样例解释
[goodykc]
[goodykcu]
[goodykcul]
g[oodykcul]
go[odykcul]
goo[dykcul]
good[ykcul]
good[lucky]
good[lucky](光标右边没有字符,失败删除)
good[luck]y
good[luck]
goodluck
5.7 数据规模与约定
• 对于40% 的数据:1 N , 初始文本长度 100,数据不包含翻转(Reverse)操作;
• 另有30% 的数据:1 N , 初始文本长度 105,数据不包含翻转(Reverse)操作;
• 另有20% 的数据:1 N , 初始文本长度 105,数据包含翻转(Reverse)操作;
• 对于100% 的数据:1 N , 初始文本长度 4 106,输出文件大小 20MB;
【解题报告】
解法一
不考虑翻转操作,我们不难发现剩下的功能都是链表所较容易支持的。故我们考虑设计一
个链表来维护文本,并记录两个光标的位置,从而可以在常数时间内完成删减及移动操作。
时间复杂度为O(n),期望得分70 分。
代码如下:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAXL=100000;
int N;
char s[MAXL+10];
int len;
int tcnt;
struct T{int pre,next;char c;} text[MAXL*11+10];
void del(int x)
{
text[text[x].pre].next=text[x].next;
text[text[x].next].pre=text[x].pre;
text[0].pre=-1;
}
void add(int x,char c)
{
++tcnt;
text[tcnt].pre=x;
text[tcnt].next=text[x].next;
text[text[x].next].pre=tcnt;
text[x].next=tcnt;
text[tcnt].c=c;
text[0].pre=-1;
}
struct C
{
int pos;
void pri(bool t){printf("%s\n",t?"T":"F");}
void move(bool l)
{
if(l)
{
if(text[pos].pre!=-1)
{
pos=text[pos].pre;
pri(true);
}else pri(false);
}else
if(text[pos].next)
{
pos=text[pos].next;
pri(true);
}else pri(false);
}
void insert(char c)
{
add(pos,c);
pos=text[pos].next;
pri(true);
}
void rm()
{
if(text[pos].next)
{
del(text[pos].next);
pri(true);
}else pri(false);
}
} L,R;
int main()
{
freopen("editor.in","r",stdin);
freopen("editor.out","w",stdout);
int i;
scanf("%s",&s);
len=strlen(s);
for(i=0;i<len;i++)
add(i,s[i]);
L.pos=0,R.pos=len;
scanf("%d",&N);
while(N--)
{
char c;
do c=getchar();
while(c<33||c>126);
if(c=='<'||c=='>')
{
bool left=c=='<'?1:0;
do c=getchar(); while(c<33||c>126);
if(c=='L') L.move(left);
else R.move(left);
}else if(c=='I')
{
do c=getchar(); while(c<33||c>126);
if(c=='L')
{
do c=getchar(); while(c<33||c>126);
L.insert(c);
}else
{
do c=getchar(); while(c<33||c>126);
R.insert(c);
}
}else if(c=='D')
{
do c=getchar(); while(c<33||c>126);
if(c=='L') L.rm();
else R.rm();
}else if(c=='S')
{
for(i=text[0].next;i;i=text[i].next)
putchar(text[i].c);
printf("\n");
}
}
return 0;
}
解法二
对于翻转操作,我们暴力修改链表的复杂度是O(n) 的。而翻转操作又是平衡树的看家本
领,故我们考虑使用Splay来维护文本。并通过翻转标记,使之支持翻转操作。
时间复杂度为O(nlogn),期望得分90 分。
解法三
对于n = 4 106 的数据,使用平衡树复杂度太高。我们继续考虑使用链表来维护文本。
我们考虑在进行翻转操作时,只有两个端点元素的前驱和后继指针发生了实际变化,而中间元
素的前驱和后继指针只是发生了交换。我们可以借鉴Splay的lazy标记的思想,对于每一个
翻转操作,我们只实时修改两端的元素,对于中间元素只在两端维护标记。只有当中间元素被
访问到时,才交换其前驱和后继指针,并移动标记。对于标记的消除和多个标记的情况,需要
进行相关讨论。
事实上使用标记来处理中间元素的指针交换需讨论较多的情况,较为复杂。事实上我们可
以不必维护标记。当我们发现左边下下个元素的后继指针不是左边下个元素时我们便可以交
换左边下下个元素的前驱和后继指针;类似的,当我们发现右边下下个元素的前驱指针不是右
边下个元素时我们便可以交换右边下下个元素的前驱和后继指针。这样我们就可以使用链表,
较轻松地实现区间翻转操作了。
时间复杂度为O(n),期望得分100 分。
代码如下:
#include <cstdio>
#include <cstdlib>
#include <string>
#include <cstring>
using namespace std;
#define N 10000000
int pre[N], suf[N];
char ch[N], st[N];
int idx, pos[2], cnt[2];
inline void swap(int &a, int &b) {
int c = a;
a = b;
b = c;
}
void Insert(int opt, char c) {
++idx;
ch[idx] = c;
int u = pos[opt], v = suf[u];
pre[idx] = u; suf[idx] = v;
suf[u] = idx; pre[v] = idx;
if (cnt[opt^1] >= cnt[opt]) cnt[opt^1]++;
pos[opt] = idx; cnt[opt]++;
if (pos[opt^1] == u) pos[opt^1] = idx;
puts("T");
}
void LMove(int opt) {
if (pos[opt] == 1) {
puts("F");
return ;
}
int u = pos[opt], v = pre[u];
if (suf[v] != u) swap(suf[v], pre[v]);
pos[opt] = v; cnt[opt]--;
puts("T");
}
void RMove(int opt) {
if (suf[pos[opt]] == 2) {
puts("F");
return ;
}
int u = suf[pos[opt]], v = suf[u];
if (pre[v] != u) swap(suf[v], pre[v]);
pos[opt] = u; cnt[opt]++;
puts("T");
}
void Delete(int opt) {
if (suf[pos[opt]] == 2) {
puts("F");
return ;
}
int u = pos[opt], v = suf[u], w = suf[v];
if (pre[w] != v) swap(suf[w], pre[w]);
if (cnt[opt^1] > cnt[opt]) cnt[opt^1]--;
if (pos[opt^1] == v) pos[opt^1] = u;
suf[u] = w; pre[w] = u;
puts("T");
}
void Reverse() {
if (cnt[1] - cnt[0] <= 0) {
puts("F");
return ;
}
if (cnt[1] - cnt[0] == 1) {
puts("T");
return ;
}
int a = pos[0], b = suf[a], c = pos[1], d = suf[c];
swap(pre[b], suf[b]); swap(pre[c], suf[c]);
suf[a] = c; pre[c] = a; suf[b] = d; pre[d] = b;
pos[1] = b;
puts("T");
}
void Show() {
int u = 1;
while (true) {
if (pre[suf[u]] != u) swap(pre[suf[u]], suf[suf[u]]);
u = suf[u];
if (u == 2) break;
putchar(ch[u]);
}
putchar('\n');
}
void Build() {
scanf("%s",st);
idx = 2;
pre[1] = -1; suf[1] = 2;
pre[2] = 1; suf[2] = -1;
pos[0] = pos[1] = cnt[0] = cnt[1] = 1;
int len = strlen(st);
for (int i = 0; i < len; i++) {
++idx;
ch[idx] = st[i];
pre[idx] = i == 0 ? 1 : idx - 1;
suf[idx] = i == len - 1 ? 2 : idx + 1;
}
if (len > 0) {
suf[1] = 3; pre[2] = idx;
pos[1] = idx; cnt[1] = len + 1;
}
}
inline int Dir() {
getchar();
return getchar() == 'L' ? 0 : 1;
}
int main() {
freopen("editor.in", "r", stdin);
freopen("editor.out", "w", stdout);
Build();
int m;
scanf("%d",&m);
for (int i = 1; i <= m; i++) {
getchar();
char opt = getchar();
while (opt != '<' && opt != '>' && !(opt >= 'A' && opt <= 'Z')) opt = getchar();
if (opt == '<') LMove(Dir());
else if (opt == '>') RMove(Dir());
else if (opt == 'I') {
int d = Dir();
char c = ' ';
while (c < 33 || c > 126) c = getchar();
Insert(d, c);
}
else if (opt == 'D') Delete(Dir());
else if (opt == 'R') Reverse();
else if (opt == 'S') Show();
}
return 0;
}