阅读视图

发现新文章,点击刷新页面。
🔲 ☆

vue组件和插槽之间的通信

vue高度封装的语法也会有很多弊端,父组件与插槽之间的通信就是其中之一。

应用场景?

对组件进行封装时, 我们往往希望父组件和子组件有所关联。

如一个选择器组件select

1
2
3
4
5
<my-select v-model="selectedData">
<my-option :value="data1"/>
<my-option :value="data2"/>
<my-option :value="data3"/>
</my-select>

且不考虑UI如何实现, 看数据流。

在select组件中, 希望其插槽中可以放若干个option组件,option组件可以绑定数据,选择option后select能获得对应数据。

仔细一想会发现, vue目前的插槽机制做不到这一点。

  • 组件和插槽内容的直接通信: vue中插槽内容完全是由外界定义的,而常见通信方法如propsemit都需要在已知结构的情况下绑定。换句话说,option没有直接途径,告诉select自己被选中了。
  • vue中插槽是一对一的(导致结构样式编写上的不便)。由插槽内容定义者在插槽中写入的option数量来决定插槽的数量,原组件可以访问到每个插槽内容并自行渲染结构(类似jsx), 听起来很美,但做不到。

vue中组件和插槽唯一的直接通信机制是作用域插槽,但这并不是组件和插槽内容的通信, 而是组件和插槽定义者之间的通信,使定义者可以获取插槽所在的组件的一些数据,进而手动将数据传递给插槽内容。

万能通信方法

那些UI框架是如何实现这一点的呢?

答案比较暴力, 使用provide/injectapi来传递一个公共对象。

比如在select中,provide一个包含了selectedDataref的对象

1
2
3
4
const selectedData = ref(null);
provide('my-select', {
selected: selectedData
})

在子组件中接收使用

1
2
3
4
const mySelectContext = inject('my-select');
function click() {
mySelectContext.selected.value = props.value;
}

这样就实现了插槽组件数据对父组件数据的绑定。

这种通信方式非常灵活, 你可以通过传递方法、被包装的对象给子组件等等,实现任何方向的通信。

实际应用

element-plus中, 有一个工具hook:getOrderedChildren, 见名知意,作用是返回组件的插槽子组件数组, 并保持其在html结构上的顺序。

以下仍以selectoption的父子关系举例如何实现。


  1. 获取所有的子组件数据

在select中定义一个ref数组, 并通过provide提供给子组件

1
2
3
4
5
// select.vue
const orderedChildren = shallowRef<OptionContext[]>([]);
provide('SelectContext', {
orderedChildren
})

option中注入,并在组件mounted的时候将自己的数据添加至其中

1
2
3
4
5
6
7
8
// option.vue
const selectContext = inject('SelectContext');

onMounted(() => {
selectContext.orderedChildren.push({
data: props.value
})
})

这样最基本的功能就实现了。

但考虑到插槽内的option可能是动态的,因此需要添加删除功能。

option中,将组件的唯一标识uid添加到数据中

1
2
3
4
5
6
7
// option.vue
onMounted(() => {
selectContext.orderedChildren.push({
uid: getCurrentInstance().uid,
data: props.value
})
})

父组件即可以根据uid来执行删除操作

1
2
3
4
5
6
7
8
//select.vue

const removeChild = (uid: number) => {
delete children[uid]
orderedChildren.value = orderedChildren.value.filter(
(children) => children.uid !== uid
)
}

将方法传递给子组件, 子组件在unmount时调用,便实现了子组件对数据的生命周期的自动管理。

1
2
3
4
5
6
7
8
9
10
// select.vue
provide('SelectContext', {
orderedChildren,
removeChild
})

// option.vue
onUnmounted(() => {
selectContext?.removeChild(instance.uid)
})

  1. 保持子组件在html结构上的顺序

vue中并没有提供直接的api访问组件的dom树,所以需要自己去拿到组件的实例, 遍历其虚拟dom树。

通过getCurrentInstance()方法, 我们能拿到组件的实例,结构如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
export interface ComponentInternalInstance {
uid: number;
type: ConcreteComponent;
parent: ComponentInternalInstance | null;
root: ComponentInternalInstance;
appContext: AppContext;
/**
* Vnode representing this component in its parent's vdom tree
*/
vnode: VNode;
/* removed internal: next */
/**
* Root vnode of this component's own vdom tree
*/
subTree: VNode;
/**
* Render effect instance
*/
effect: ReactiveEffect;
/**
* Bound effect runner to be passed to schedulers
*/
update: SchedulerJob;
proxy: ComponentPublicInstance | null;
exposed: Record<string, any> | null;
exposeProxy: Record<string, any> | null;
/* removed internal: withProxy */
/* removed internal: ctx */
data: Data;
props: Data;
attrs: Data;
slots: InternalSlots;
refs: Data;
emit: EmitFn;
attrsProxy: Data | null;
slotsProxy: Slots | null;
isMounted: boolean;
isUnmounted: boolean;
isDeactivated: boolean;
}

主要看subTree这个属性,它存放的便是从当前组件开始的虚拟dom树。

VNode便是vue的虚拟dom树的结点, 结构如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
export interface VNode<HostNode = RendererNode, HostElement = RendererElement, ExtraProps = {
[key: string]: any;
}> {
/* removed internal: __v_isVNode */

type: VNodeTypes;
props: (VNodeProps & ExtraProps) | null;
key: string | number | symbol | null;
ref: VNodeNormalizedRef | null;
/**
* SFC only. This is assigned on vnode creation using currentScopeId
* which is set alongside currentRenderingInstance.
*/
scopeId: string | null;
/* removed internal: slotScopeIds */
children: VNodeNormalizedChildren;
component: ComponentInternalInstance | null;
dirs: DirectiveBinding[] | null;
transition: TransitionHooks<HostElement> | null;
el: HostNode | null;
anchor: HostNode | null;
target: HostElement | null;
targetAnchor: HostNode | null;
/* removed internal: staticCount */
suspense: SuspenseBoundary | null;
/* removed internal: ssContent */
/* removed internal: ssFallback */
shapeFlag: number;
patchFlag: number;
/* removed internal: dynamicProps */
/* removed internal: dynamicChildren */
appContext: AppContext | null;
/* removed internal: ctx */
/* removed internal: memo */
/* removed internal: isCompatRoot */
/* removed internal: ce */
}

其中children便是结点的所有子元素数组,且在顺序上与真实dom结构是一致的。

因此我们可以遍历dom树, 找出所有的option元素,这样就得到了顺序。

为了给option组件一个唯一的标识, 以支持我们的查找,可以为option定义一个name

1
2
3
4
// option.vue
defineOptions({
name: 'trudbot-option'
})

element-plus中, 是以扁平化虚拟dom树,然后过滤来实现查找的。

1
2
3
4
5
6
const nodes = flattedChildren(vm.subTree).filter(
(n): n is VNode =>
isVNode(n) &&
(n.type as any)?.name === childComponentName &&
!!n.component
)

改变添加组件的策略, 添加数据时, 先以uid为键保存子组件数据,然后遍历dom树得到顺序,在根据顺序得到数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const children: Record<number, T> = {}
const orderedChildren = shallowRef<T[]>([])

const addChild = (child: T) => {
children[child.uid] = child
const nodes = flattedChildren(vm.subTree).filter(
(n): n is VNode =>
isVNode(n) &&
(n.type as any)?.name === childComponentName &&
!!n.component
)
const uids = nodes.map((n) => n.component!.uid)
orderedChildren.value = uids.map((uid) => children[uid]).filter((p) => !!p)
}

同样, 删除组件时, 也把组件的uid记录删除

1
2
3
4
5
6
const removeChild = (uid: number) => {
delete children[uid]
orderedChildren.value = orderedChildren.value.filter(
(children) => children.uid !== uid
)
}

provide增加和删除组件的方法, 而不直接传递数组

1
2
3
4
provide('SelectContext', {
addChild,
removeChild,
})

至此,父组件在任何情况下都能通过orderedChildren访问到插槽内option的数据,因为数据会随option的生命周期自动管理。


有什么用?

这是一个通用的hooks,能够帮助组件轻松的访问到插槽内指定组件提供的数据,而且组件数据的顺序是稳定的。

此hook在element-pluscarouseltabs等组件中均有应用。

普通的插槽通信, 也许直接provide就够用了;而通信较复杂时,可以使用此hook, 清晰、稳定。

参考

element-plus/packages/hooks/use-ordered-children(github.com)

依赖注入| Vue.js (vuejs.org)

🔲 ☆

git commit message 规范

commit message的规范格式很有必要,让别人和未来的自己都能更快速的浏览commits。在此记录相关规则供自查。

规范

1
2
3
<type>(<scope>): <subject>
// 空一行
<body>

type

本次commit的归类。

  • Feat– feature | 新功能

  • Fix– bug fixes | 修补bug

  • Docs– changes to the documentation like README |文档

  • Style– style or formatting change |格式(不影响代码运行的变动)

  • Perf – improves code performance |优化相关,比如提升性能、体验。

  • Test– test a feature | 增加测试

  • ReFactor- A code that neither fix bug nor adds afeature. | 重构(即不是新增功能,也不是修改bug的代码变动)

  • Chore: Changes that do not affect the external user| 构建过程或辅助工具的变动。

  • Revert:回滚

  • Merge:代码合并。

scope

The “scope” field should be a noun that represents the part of thecodebase affected by the commit.

For example, if the commit changes the login page, the scope could be“login”. If the commit affects multiple areas of the codebase, “global”or “all” could be used as the scope.

subject

subject是 commit 目的的简短描述,不超过50个字符。

Body

Body 部分是对本次 commit 的详细描述,可以分成多行。

🔲 ☆

react bug记录: useEffect中使用响应式变量

在使用useEffect API时出现了一些错误, 折腾好一会,只能感慨还是看文档不认真。

代码重现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1. 创建state
const [groups, setGroups] = useState([]);
2. 通过useEffect创建连接
useEffect(() => {
const connection = createUnreadMessageConnection({useinfo });
connection.on('remind', (groupId: number) => {
3. 监听事件
// 使用groups 值do something
}
return () => {
connection.disconnect();
}
}, [useInfo.state]);
4. 异步请求数据
getUserGroup({ ... }).then(res => {
setGroups(res)
})
})

以上是相关代码的执行流程。

代码中的bug最终反映在前端界面上, 经过排查, 发现是3处,groups始终是空数组也就是初始值。

这个bug原因也很简单, 在将事件从Effect 中分开 – React 中文文档 (docschina.org)中提到

Effect读取的每一个响应式值都必须在其依赖项中声明

也就是说在useEffect中读取的每一个响应式值都必须作为其依赖。

注意响应式值并不一定是state,也可以是组件内部使用state计算得到的变量、函数等。


要解决这个bug, 似乎将groups放入依赖列表就可以了,就这么简单吗?

为什么要在useEffect建立连接呢, 因为如果放到组件函数体内,组件每一次渲染,就会发起一次连接, 同时上一次连接会断开。

但从逻辑上, 只有当登录的用户信息变化时, 才需要重新建立连接,所以useEffect非常适合这一个场景。

如果把groups放入依赖中, 每次groups变化, 都会重新连接,这便又破坏了我们的连接逻辑。

来看useEffect中的代码。

1
2
3
4
5
const connection = createUnreadMessageConnection({useinfo });
connection.on('remind', (groupId: number) => {
3. 监听事件
// 使用groups 值do something
}

每次useinfo变化, 我们希望连接重新进行。 effect中的代码是响应式的,但我们此刻希望, 即能通过依赖, 让一些逻辑保持响应式;又可以在不依赖某些响应式值的状态下,访问到它们的最新值(非响应式代码获取最新值)。

你需要一个将这个非响应式逻辑和周围响应式 Effect 隔离开来的方法。

解决方法: 包装

useEffectEvent

将事件从Effect 中分开 – React 中文文档 (docschina.org)中,提供了useEffectEvent API的用法。

简单说就是, 将非响应式代码包装到useEffectEvent 中,然后在useEffect中调用, 就可以不影响Effect响应式逻辑的情况下,去获取state的最新值。

从名字也可以看出来, 这是用在useEffect中的事件处理函数,完美符合我们的需求。

你可以将 Effect Event看成和事件处理函数相似的东西。主要区别是事件处理函数只在响应用户交互的时候运行,而Effect Event 是你在 Effect 中触发的。Effect Event 让你在 Effect响应性和不应是响应式的代码间“打破链条”。

1
2
3
4
5
6
7
8
const onRemind = useEffectEvent((groupId) => {
// 使用groups做些什么
});

...
connection.on('remind', (groupId: number) => {
onRemind(groupId);
}

但此API仍处于实验性, 还没有进入正式版本。

Reducer

使用Reducer在某些情况下也可以实现对state的包装, reducer被创建后,本身一般是不会被改变的, 它接收用户的操作, 在内部对state进行更改。因此我们在useEffect内部, 可以通过Reducer这一中间层,间接的对state进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const groupDataReducer = (state, action) => {
switch(action) {
case "remind":
// do something
}
}

...
const [state, dispatch] = useReducer(groupDataReducer, {
groups: [],
});

connection.on('remind', (groupId: number) => {
dispatch({
type: 'remind',
value: groupId
})
}

我真幸运, 第一次写react就遇到了react的小缺陷(maybe)。

🔲 ⭐

nest中使用websocket

本文主要总结nest中如何使用socket.io等WebSocket库,socket.io的使用可见socket.io

网关注解

要在nest中写websocket相关的代码,需要创建一个被@WebSocketGateway()装饰的类,里面封装了websocket服务基本的API。

1
2
@WebSocketGateway({})
export class ChatGetWay {}

在类中有三个固定名称的钩子函数, 控制着websocket的生命周期:

  1. afterInit(), 参数为服务器实例
  2. handleConnection(), 参数为客户端实例
  3. handleDisconnect(), 参数为客户端实例

作用顾名思义。

Socket.io配置

在使用socket.io创建服务器时, 往往需要进行相关配置如跨域等

1
2
3
4
5
6
7
8
// server-side
const io = new Server(httpServer, {
cors: {
origin: "https://example.com",
allowedHeaders: ["my-custom-header"],
credentials: true
}
});

在nest中, 只需要将配置对象传入注解即可。

1
2
3
4
5
6
7
@WebSocketGateway({
cors: {
origin: '*',
methods: ['GET', 'POST']
},
namespace: 'chat'
})

在网关的第一个参数可以设置websocket的端口

1
@WebSocketGateway(80, { namespace: 'events' })

当然, 如果你不传入此参数,websocket服务器会自动挂载到nest启动的http服务器上, 监听相同的端口。

Socket.io业务

socket.io中最重要的就是发送和接收事件消息了。 在nest中,需要使用@SubscribeMessage()注解来装饰一个函数,使其成为事件处理函数。

1
2
3
4
5
6
7
@SubscribeMessage('message') // 订阅`message`事件
handleMessage(
@ConnectedSocket() client, // 发送消息的客户端引用
@MessageBody() data // 事件携带的信息
) {
// do something
}

上面使用了两个注解来修饰事件处理函数的参数,这是因为websocket网关是跨平台的, 平台不同,客户端实例和消息格式也都可能不同。

socket.io服务器实例

要在nest中使用socket.io发送消息, 需要直接访问到服务器实例。

使用@WebSocketServer()注解修饰成员变量,即可获得服务器实例。

1
2
@WebSocketServer()
server: Server;

更多的与平台API相关的操作都可以通过服务器实例来完成。

1
this.server.to(group).emit('message', msg);

使用WebSocket服务

WebSocket网关类是特殊的Provider,也就是类似于被@Injectable()的类, 使用上是类似的。可以通过构造函数注入其它Service、被模块管理,也可以被注入。

websocket和http同端口时的跨域问题

如果你的后端中http和websocket是使用的同一端口,即时你nest实例和websocket网关都配置了跨域,你在使用客户端的时候还是会发生跨域。

这是以为socket.io客户端进行连接时, 首先会尝试建立HTTP长轮询连接,连接成功后才会尝试建立WebSocket连接。

而同一端口已经存在了处理HTTP的服务器,因此客户端尝试建立HTTP长轮询时, 就会发生错误。

解决办法是, 在客户端创建连接时配置, 只允许建立WebSocket连接。

1
const connection = io(url, {transports: ['websocket'] });

或者, 给socket.io服务器一个单独的端口。

参考

WEBSOCKETS(nestjs.cn)

介绍 | Socket.IO

客户端配置| Socket.IO

🔲 ⭐

RMQ

全称是Range Minimum/Maximum Query,即"区间最大最小值问题", 一般来说需要处理多组查询,查询的区间长度不一、可能重复。

这里我们以模板举例:

原题link

题目描述

给定一个长度为的数列,和 次询问,求出每一次询问的区间内数字的最大值。

输入格式

第一行包含两个整数 ,分别表示数列的长度和询问的个数。

第二行包含 个整数(记为),依次表示数列的第 项。

接下来 行,每行包含两个整数,,表示查询的区间为

线段树

线段树基于分治的思想, 是解决的经典数据结构。

此前已有总结, 具体见线段树.

单调栈

单调栈可以以离线的方式解决问题。

根据单调栈的原理我们知道, 当我们把~插入到单调栈后,会得到一个索引递增的, 且值递增/递减的序列。

我们可以将所有查询读入, 并按照右端点排序。

依次插入到单调栈中, 在这个过程中,如果插入的下标来到了某个查询的右端点,此时我们在单调栈中二分到第一个下标的元素, 这就是的答案。

对于区间最大值问题, 我们需要维护一个递减的单调栈, 然后二分找到在[l,r]区间的第一个元素。

时间复杂度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int a[N], stk[N], top = -1, t = 0;
struct Q{int id, l, r;};

inline int read() {
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}

void push(int i) {
while (top != -1 && a[stk[top]] <= a[i]) top --;
stk[++ top] = i;
}

int query(int b, int e) {
while (t < e) push(++ t);
int l = 0, r = top;
while (l < r) {
int mid = (l + r) >> 1;
if (stk[mid] >= b) r = mid;
else l = mid + 1;
}
return a[stk[l]];
}

int main () {
int n = read(), m = read();
for (int i = 1; i <= n; i ++) a[i] = read();
vector<Q> q;
for (int i = 0; i < m; i ++) {
q.push_back({i, read(), read()});
}
sort(q.begin(), q.end(), [](auto &a, auto &b) {
return a.r < b.r;
});
vector<int> res(m);
for (auto &x : q) {
res[x.id] = query(x.l, x.r);
}
for (int &x : res) printf("%d\n", x);
return 0;
}

ST表

ST表(Sparse Table,稀疏表), 是一种基于倍增思想,用于解决可重复贡献问题的数据结构。

ST表一般用来解决区间性质查询问题,并且要求这个区间性质是可重复贡献的、可结合(拆分)的。

  • 可结合(拆分): 区间的性质能由子区间的性质组合而成。
  • 可重复贡献: 某个子区间贡献多次, 并不会影响结果。

例如经典的区间最大值问题:

  1. 区间的最大值能由若干个能覆盖区间的子区间取最大值得到
  2. 取的子区间有重合部分时, 并不会影响结果

接下来, 我们讲ST表的思路:

为具体的数据的性质, 为区间[l, r]的性质。

表示],由倍增思想我们知道

我们可以用的时间复杂度预处理得到倍增数组。

在查询区间的性质时,我们知道只需要将其拆分为若干个区间, 分别求出性质,再进行组合就可以了。

那么如何利用已经求出的倍增数组呢?

在普通的倍增中, 一般会选择将区间拆分为若干个2的幂次的长度的区间,这些区间在倍增数组中都已记录, 但这样每次查询的时间复杂度为

这时候可重复贡献的性质就很重要了, 为了降低时间复杂度,我们可以只将区间拆分为两个区间: , 其中

长度为2的幂次, 所以倍增数组中已经求出对应区间的性质; 而一定是大于等于的,所以两个区间一定能覆盖[l, r]

时间复杂度

预处理为,每次查询为,所以时间复杂度为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int f[N][21];

inline int read() {
int x=0,f=1;char ch=getchar();
while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}

int main () {
int n = read(), m = read();
for (int i = 1; i <= n; i ++) f[i][0] = read();
for (int j = 1; j < 21; j ++)
for (int i = 1; i + (1 << j) - 1 <= n; i ++)
f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
while (m --) {
int l = read(), r = read();
int len = log2(r - l + 1);
printf("%d\n", max(f[l][len], f[r - (1 << len) + 1][len]));
}
return 0;
}

参考

ST 表 - OI Wiki(oi-wiki.org)

浅谈ST表 -自为风月马前卒 - 博客园 (cnblogs.com)

算法学习笔记(12):ST表 - 知乎 (zhihu.com)

🔲 ⭐

2023 RoboCom 世界机器人开发者大赛-本科组 (省赛) CAIP 题解

RoboCom省赛题解。

RC-u1 亚运奖牌榜

2022 年第 19届亚运会即将在杭州召开,杭州已经做好准备欢迎全亚洲的观众一同参与亚运盛会了!

你正在开发一款跟亚运奖牌计算相关的App。给定两个国家的获奖情况,你的任务是计算这两个国家/地区的奖牌情况,并确定哪个国家/地区要排在奖牌榜的前面。

输入格式

输入第一行是一个正整数 N (1≤N≤1000),表示总共有 N 条获奖记录。

接下来的每一行都是形如以下的一条记录: 其中 ,0 表示是第一个国家/地区,1表示是第二个国家/地区;,1表示金牌,2 表示银牌,3表示铜牌。

输出格式:

首先输出两行,第一行是第一个国家/地区的金牌、银牌、铜牌获得数,用空格隔开;第二行是第二个国家/地区的奖牌获奖情况,要求与格式同第一个国家/地区。

最后一行,如果是第一个国家/地区排在前面,输出The first win!,否则输出 The second win!

排在前面的定义是:先比较金牌数,金牌数较大的排在前面;如金牌数相等,比较银牌数,银牌数较大的在前面;如金牌银牌数都相等,则比较铜牌数,铜牌数较大的在前面。

保证数据不存在奖牌数完全相同的情况。

输入样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
15
0 1
0 2
0 3
0 1
0 1
0 2
0 3
1 3
1 3
1 3
1 3
1 2
1 1
1 1
1 1

输出样例:

1
2
3
3 2 2
3 1 4
The first win!

题解 - 模拟

用vector存储每个人的奖牌情况, 然后排序。

时间复杂度:

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//
// Created by trudbot on 2023/7/15.
//

#include <bits/stdc++.h>
using namespace std;

int main () {
int n; cin >> n;
vector<vector<int>> ans(2, vector<int>(4, 0));
while (n --) {
int c, p; cin >> c >> p;
ans[c][p] ++;
}
for (auto &v : ans) {
for (int i = 1; i <= 3; i ++) {
cout << v[i];
if (i != 3) cout << " ";
}
cout << endl;
}
auto t = ans;
sort(ans.begin(), ans.end());
if (t == ans) cout << "The second win!";
else cout << "The first win!";
return 0;
}

RC-u2 出院

1
2
3
4
5
A:最近出了一个饮料营养等级你们知道吗?例如无糖的饮料是 A 级,可乐是 D 级……
B:那……无糖可乐是什么级别?
C:AD 级吧。
A:出院!
B:出什么院,你也给我进去!

以上是某群中一段有趣的对话。请你按照里面的逻辑,在已知某些饮料的等级的情况下,给饮料定级。定级的方法是:

  • 如果是已知等级的饮料,直接输出等级;
  • 对于一个新饮料的名字,你需要将名字拆成两个已知等级的部分,然后输出这个级别。例如:Diet是A,Coke是D,那么DietCoke就是AD;
  • 如果新饮料无法拆解或者有多种拆解方法,统一定为 D 级。

输入格式:

输入第一行是两个正整数 N,M(1≤N,M≤100),表示已知的饮料有 N种,需要定级的饮料有 M 种。

接下来首先是 N行,每行是一个字符串和一个字符,表示一种饮料的名字和对应的等级,等级只有A,B,C,D 四种。

然后是 M行,每行是一个字符串,表示需要定级的饮料的名字。

所有饮料名字只包含有大小写字母,长度不超过30,给定拥有等级的饮料的名字不会重复。

输出格式:

对于每一个需要定级的饮料,输出定好的定级。

输入样例:

1
2
3
4
5
6
7
8
9
10
11
12
5 6
Diet A
LowSugarTea B
Milk C
Coke D
Water A
DietCoke
Pepsi
Milk
CokeWater
GoodMilk
dietCoke

输出样例:

1
2
3
4
5
6
AD
D
C
DA
D
D

题解 - 模拟

用哈希表存储已知的每个字符串的等级。

对于需要判断的字符串s

  • 首先判断是否已知, 若为真直接输出即可
  • 枚举每个已知等级的字符串,判断其是否为s的前缀,若为前缀则判断s除去这部分前缀后的部分是否已知等级,若都已知, 则拼接
  • 此过程中记录拼接的次数, 若不为一则定为D级。

时间复杂度

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//
// Created by trudbot on 2023/7/15.
//
#include <bits/stdc++.h>
using namespace std;
#define x first
#define y second

int main () {
int n, m; cin >> n >> m;
map<string, string> level;
for (int i = 0; i < n; i ++) {
string s, lev; cin >> s >> lev;
level[s] = lev;
}
for (int i = 0; i < m; i ++) {
string s; cin >> s;
//如果已知等级
if (level.count(s)) cout << level[s] << endl;
else {
int cnt = 0;
string res;
for (auto &p : level) {
if (p.x.size() < s.size()
&& s.substr(0, p.x.size()) == p.x //是否为前缀
&& level.count(s.substr(p.x.size()))) //后缀是否已知
cnt ++, res = p.y + level[s.substr(p.x.size())];
}
if (cnt != 1) res = "D";
cout << res << endl;
}
}
return 0;
}

RC-u3 骰子游戏

在某个游戏中有一个骰子游戏。在游戏中,你需要投掷 5个标准六面骰子(骰子为一个正方体,6个面上分别有1、2、3、4、5、6中的一个数字,骰子的质量均匀),投出的点数根据组合会获得一个“获胜等级”。获胜等级从高到低如下:

  • 五个同点数 - 五个骰子显示相同的点数
  • 四个同点数 - 四个骰子显示相同的点数
  • 葫芦 - 一对和一个三个同点数(如1、1、3、3、3)
  • 六高顺子 - 投出的点数为 2、3、4、5、6
  • 五高顺子 - 投出的点数为 1、2、3、4、5
  • 三个同点数 - 三个骰子显示相同的点数(如1、1、1、2、3)
  • 两对 - 投出的点数中有两对是相同的(如 1、1、2、2、3)
  • 一对 - 投出的点数有一对是相同的(如 1、1、2、3、4)
  • 无 - 除去以上的其他情况

给定你已经投出的一次结果,现在假设你可以选择任意个骰子重投一次,请问怎么样操作,才能最大化在重骰后获得更好的获胜等级的概率呢?

注意:更好的获胜等级需要严格地比当前的获胜等级更好,例如1、1、2、2、3 如果重骰后变为 1、1、3、3、4并不比当前的获胜等级更好。

输入格式:

输入第一行是一个正整数 T(1≤T≤10),表示接下来有多少组数据。 每组数据只有一行 5个数字,表示第一次投出的 5 个骰子的点数。

输出格式:

对于每组数据输出三个整数,其中第一个整数为为了获得最大的概率需要重新骰几个骰子,后面的两个整数为重骰骰子后概率的最简分数,其中第二个整数为分子,第三个整数为分母。如果分子为0,分母为 1。

如果有多种获得最大概率的情况,取重骰的骰子数最少的方案。

输入样例:

1
2
3
4
3
1 1 2 2 3
1 1 2 3 4
1 1 1 2 3

输出样例:

1
2
3
3 4 9
3 13 18
2 4 9

样例说明:

样例的第一组数据中,一种方案是:重骰最后三个骰子以获得最大的概率(只要重骰的有一个“1”或者三个均相等即可)。

RC-u4 相对论大师

在某个直播间里,观众常常会发送类似这样的弹幕:

1
鱼越大,鱼刺越大;鱼刺越大,肉越少;肉越少,鱼越小;所以鱼越大,鱼越小

这样通过一连串推导得出一个搞笑的结论的弹幕发送者被称为“相对论大师”。

现在给定一系列已有的推论,请你从给定的推论中挑选一些,组成一条类似于上面的弹幕,成为一名“相对论大师”。

输入格式:

输入第一行是一个正整数 (1≤≤1000),表示总共有多少条推论。

接下来的 行,每行有两对四个元素,形如下:

A 0 B 1 每对元素表示一个论点:第一个是一个长度不大于 5的、只包含大小写字母的字符串,称为论点的核心;第二个数字固定为 0 或者1,代表论点核心的方向属性。为简单理解,你可以将 0 理解为正面方向,1理解为负面方向。例如:

1
YuCi 0 Rou 1

就可以理解为鱼刺大,肉少

于是一行中的两个论点就形成一条推论,表示第一个核心某个方向的属性能推出第二个核心的某个方向的属性,即鱼刺越大,肉越少

输出格式:

按照弹幕格式输出一行,例如:

1
Yu 0 YuCi 0 YuCi 0 Rou 1 Rou 1 Yu 1 = Yu 0 Yu 1

具体格式要求为:在一行中输出从起始论点到最终论点的所有推论,论点格式与输入相同,论点间以1个空格分隔。随后输出等号(等号前后均有1个空格),最后是相互矛盾的起始和终止论点。

如果有多种方案,选择使用推论最少的;推论条数相同的输出任意一种方案均可。

在方案中每条推论仅可使用一次。保证有解,且给定的推论中没有相同的推论。

输入样例:

1
2
3
4
5
6
5
Yu 0 Yuci 0
Rou 1 Yu 1
Yuci 0 Rou 1
Yuci 0 Gutou 0
Gutou 0 Rou 0

输出样例:

1
Yu 0 Yuci 0 Yuci 0 Rou 1 Rou 1 Yu 1 = Yu 0 Yu 1

题解 - bfs

题意: 一个单向图, 每两个顶点为一组,要求找到所有同一组内的顶点路径中的最短路径, 并输出。

本题的考点在于如何建图,以及使用bfs找到并记录两个顶点的最短路径。

对于每个新论点, 我们给其2个未使用的相邻的顶点编号,并且以较小的编号为基本编号。如yuci的基本编号为1,那么yuci 0的编号就为1,yuci 1的编号为2。 同时我们记录每个编号的名称,以及每个名称的基本编号,这样我们就完成了图的建立和相关信息的保存。

然后, 我们枚举每一个顶点组, 使用bfs获得最短路径,在所有路径中取最短的有效路径为答案。

最后, 根据路径输出对应的信息即可。

时间复杂度:

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <bits/stdc++.h>
using namespace std;
const int N = 4010;
int n, m;
string name[N];
vector<int> g[N];
unordered_map<string, int> id;

vector<int> bfs(int start, int end) {
queue<int> q;
vector<int> last(n + 1, 0);
q.push(start);
while (!q.empty()) {
int u = q.front(); q.pop();
if (u == end) break;
for (auto &v : g[u])
if (last[v] == 0)
last[v] = u, q.push(v);
}
vector<int> res;
do {
res.push_back(end), end = last[end];
} while (end != 0);
return res;
}

void print(int u, bool space) {
cout << name[u] << " " << u - id[name[u]];
if (space) cout << " ";
}

int main () {
cin >> m;
//编号、建图
for (int i = 1; i <= m; i ++) {
string s1, s2;
int st1, st2;
cin >> s1 >> st1 >> s2 >> st2;
if (!id.count(s1)) {
n += 2, id[s1] = n - 1, name[n - 1] = name[n] = s1;
}
if (!id.count(s2)) {
n += 2, id[s2] = n - 1, name[n - 1] = name[n] = s2;
}
g[id[s1] + st1].push_back(id[s2] + st2);
}
//取最短路径
vector<int> res(2000);
for (int i = 1; i <= n; i += 2) {
auto p1 = bfs(i, i + 1), p2 = bfs(i + 1, i);
if (res.size() > p1.size() && p1.size() > 1) res = p1;
if (res.size() > p2.size() && p2.size() > 1) res = p2;
}
//打印格式
for (int i = res.size() - 1; i >= 1; i --) {
print(res[i], true), print(res[i - 1], true);
}
cout << "= ", print(res.back(), true), print(res[0], false);
return 0;
}

RC-u5 相对成功与相对失败

注意:题面内容表达纯属娱乐,与现实无关!

网上常有人说:看 XX只能度过一个相对成功/失败的人生。不妨假设把这个句式套用在“参加睿抗比赛“以及“玩手机游戏”上,那么有:

  • “参加睿抗比赛”必然比“不参加睿抗比赛”要成功;
  • “玩手机游戏“必然比“不玩手机游戏”要失败。

现在有 N个人,已知这些人自己填写的是否参加了睿抗比赛以及是否玩手机游戏的情况,以及他们实际上的成功程度的排序顺序,请问最少有多少人在填写情况时说谎了?

输入格式:

输出第一行为一个正整数 (1≤≤5),表示数据组数。

每组数据第一行是一个正整数 (1≤),表示总共的人数。

接下来的 N 行,第 i 行有两个数字 ,表示第 位参赛选手是否参加了睿抗比赛以及是否玩手机游戏,0 为没有参加/没有玩,1为参加了/玩了。

最后一行有 个数,为一个选手编号 1 到 的排列,表示选手成功程度的排序。排序顺序从最成功到最失败。

选手编号从 1 开始。

输出格式:

对于每组数据,输出一个整数,表示最少的说谎人数。

输入样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
3
5
1 0
1 0
0 0
0 0
0 1
1 2 3 4 5
5
1 0
1 0
0 0
0 0
0 1
5 4 3 2 1
5
1 0
0 1
0 0
0 1
1 1
4 2 1 3 5

输出样例:

1
2
3
0
3
2

样例说明:

对于样例中的第三组数据,一种可能是编号为 4 的选手和编号为 2的选手说谎了。

题解-DP

根据

  • “参加睿抗比赛”必然比“不参加睿抗比赛”要成功;
  • “玩手机游戏“必然比“不玩手机游戏”要失败。

我们可以为每个人算出一个得分, 若“参加睿抗比赛”则得一分,“不玩手机游戏”则再得一分。

排序应该是根据分数不上升的排列的。

现给定了一个排列, 问最少有多少人撒谎。

换种方式理解, 也就是最多有多少人没有撒谎,即最多有多少人是符合分数不上升排列的。

所以本问题即是经典问题: 最长不上升/下降子序列长度。

用朴素的最长不上升子序列做法时间复杂度是, 若要优化为更是麻烦。

注意到本题中, 分数只可能取0, 1, 2三种值。

所以可以换一种方式dp, 定义为当前以分数i结尾的最长不上升子序列长度。

由于不上升, i之前的分数必须不小于i,则

时间复杂度为

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//
// Created by trudbot on 2023/7/15.
//
#include <bits/stdc++.h>
using namespace std;

int main () {
int T; cin >> T;
for (int t = 1; t <= T; t++) {
int n; cin >> n;
vector<int> s(n + 1, 0);
for (int i = 1; i <= n; i ++) {
int a, b; cin >> a >> b;
s[i] = a + 1 - b;
}
vector<int> dp(3, 0);
int mx = 0;
for (int i = 0; i < n; i ++) {
int x; cin >> x;
for (int j = s[x]; j < 3; j ++)
dp[s[x]] = max(dp[s[x]], dp[j] + 1);
mx = max(dp[s[x]], mx);
}
cout << n - mx << endl;
}
return 0;
}
🔲 ⭐

FFT

先来回忆一下高精度乘法的原理

对于正整数相乘,将A视为, B同理为, 将A * B视作多项式相乘,得到C的多项式形式为,然后逐一进位, 即可得到C的值。

其中,

这种方法的时间复杂度为级别, 而FFT可以以的复杂度计算得到多项式乘积。

多项式的点值表示

n次多项式

由线性代数知识可知,若知道了n+1组不同的[x, f(x)]的关系,即可得到一个n+1 x n+1的多元线性方程组,且这个方程组有唯一解。

也就是说, 对于一个n次多项式, 我们可以用来定义它, 其中

n次多项式m次多项式, 由于的结果最多是n + m次,所以我们将两个多项式进行零填充到n + m次, 此时如果 可以看到, 在点值形式下, 计算仅需,如果我们能快速地将多项式转换为点值形式, 计算后,再将结果快速地转换为系数形式,就能以较低的时间复杂度完成多项式的计算。

离散傅里叶变换(DFT)

我们要将n次多项式转换为点值形式, 该怎么做呢?

可以任意取n + 1个x值, 带入进行计算,但这样时间复杂度是

数学家傅里叶取了一组特殊的点带入, 使得其可以进行优化。

单位根

若复数满足, 则称为n次单位根。

将单位圆n等分,即可得到圆上的n个坐标点,而这n个坐标点表示的复数都是n次单位根。其中第k个n次单位根称为, 特变的, , 即(1,0)为起点, 逆时针. 易得

单位根的性质

在本文中需要用到单位根的如下性质(均可通过展开式推导得出):


而离散傅里叶变换就是一组单位根作为x值, 但如果还是直接代入计算的话,复杂度仍然没有变化。

快速傅里叶变换(FFT)

快速傅里叶变换是一种能在计算机中快速地计算离散傅里叶变换的算法。

FFT的基本思想是分治。

n-1阶多项式,且为2的整数幂(不够则零填充),上面我们知道要求的点值形式, 也就是对每一个k求出

按奇数次幂和偶数次幂分为两部分: 代入 故求出只需要先求出.

假设我们已经求出了gh的值,就可以求出所有的f的点值表示。

gh的问题规模都是f的一半,所以时间复杂度为

离散傅里叶逆变换(IDFT)

用矩阵乘法表示DFT的过程为 FFT结束后我们得到了最右边的结果,所以要得到系数矩阵只需要让结果乘以中间大矩阵的逆矩阵即可。

怎么求这个矩阵的逆矩阵呢?这里的结论是每一项取倒数,再除以多项式的长度n,至于证明可以参见快速傅里叶变换(FFT)超详解- 知乎 (zhihu.com)

快速傅里叶逆变换(IFFT)

结果矩阵乘以逆矩阵这个过程显然和FFT的过程很相似,考虑怎么转化。

观察,要求的IFFT, 首先求出的IFFT, 且将x取反,最后加和后再除以n, 即可完成。

所以IFFT和FFT可以公用一套代码,只需要用一个标志位来控制关键位置的取反。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//
// Created by trudbot on 2023/7/14.
//
#include "complex"

//sign为1时是FFT, -1是IFFT
//n必须为2的整数次幂
const double pi = acos(-1);
void FFT(std::complex<double> *a, int n, int sign) {
if (n == 1) return;
std::complex<double> g[n / 2], h[n / 2];
for (int i = 0, j = 0; i < n; i += 2, j ++) {
g[j] = a[i], h[j] = a[i + 1];
}
FFT(g, n / 2, sign), FFT(h, n / 2, sign);
std::complex<double> w(1, 0), u(cos(2 * pi) / n, sign * sin(2 * pi / n));
for (int i = 0; i < n / 2; i ++, w *= u) {
a[i] = g[i] + w * h[i];
a[i + n / 2] = g[i] - w * h[i];
}
}

void DFT(std::complex<double> *a, int n) {
FFT(a, n, 1);
}

void IDFT(std::complex<double> *a, int n) {
FFT(a, n, -1);
for (int i = 0; i < n; i ++) {
a[i] /= n;
}
}

参考

超详细易懂FFT(快速傅里叶变换)及代码实现_Trilarflagz的博客-CSDN博客

快速傅里叶变换(FFT)超详解- 知乎 (zhihu.com)

傅里叶变换与大数乘法- Free_Open - 博客园 (cnblogs.com)

如何通俗易懂地解释卷积?- 知乎 (zhihu.com)

如何通俗地解释什么是离散傅里叶变换?- 知乎 (zhihu.com)

快速傅里叶变换- OI Wiki (oi-wiki.org)

欧拉公式_百度百科(baidu.com)

如何用latex编写矩阵(包括各类复杂、大型矩阵)?- 知乎 (zhihu.com)

🔲 ☆

哈密顿路径问题

哈密顿路径(Hamiltonian path)和欧拉路径的定义有些相似。

哈密顿路径(通路): 仅且经过图中每个顶点一次的路径

哈密顿环(回路): 起始顶点和终止顶点相同的哈密顿路径

哈密顿图: 存在哈密顿环的图

Hamiltonian_path.svg

哈密顿路径问题和哈密顿环问题

哈密顿路径问题和哈密顿环问题是图论中的经典问题,内容分别是确定图中是否存在哈密顿路径和哈密顿环。

该问题现有的一些成果如下:

在一个阶数为n的图中,可能成为哈密顿路径的顶点序列最多有有n!个(在完全图的情况下恰好为n!个),因此暴力搜索所有可能的顶点序列是非常慢的。一个早期的在有向图上寻找哈密顿环的算法是Martello的枚举算法[3]由Frank Rubin[4]提供的搜索过程将图的边分为3种类型:必须在路径上的边,不能在路径上的边,和未定边。在搜索的过程中,一个决策规则的集合将未定边进行分类,并且决定是否继续进行搜索。这个算法将图分成几个部分,在它们上问题能够被单独地解决。

另外,Bellman,Held, and Karp动态规划算法可以在O()时间内解决问题。在这个方法中,对每个顶点集S和其中的每一个顶点v,均做出如下的判定:是否有一条经过S中每个顶点,并且在v结束束的路径,对于每一对S和v,当且仅当存在v的邻居w满足存在一条路径经过S-v所有顶点,并在上w结束的路径时,存在路径经过中S每个顶点,并且在v结束。这个充要条件已经可以之前的动态规划计算中确认。[5][6]

Andreas Björklund通过inclusion–exclusionprinciple将哈密尔顿环的计数问题规约成一个更简单,圈覆盖的计数问题,后者可以被通过计算某些矩阵的行列式解决。通过这个方法,并通过蒙特卡洛算法,对任意n阶图,可以在O()时间内解决。对于二分图,这个算法可以被进一步提升至o().[7]

对于最大度小于等于3的图,一个回溯搜索的方法可以在 O()时间内找到哈密顿环.[8]

哈密顿环和哈密顿路径也可以通过SATsolver找到.

问题的充分条件

哈密顿路径存在的充分条件:对于阶简单图, 若图中任意一对顶点都满足 ,则G中存在哈密顿路径.

哈密顿环存在的充分条件: 对于阶简单图, 若图中任意一对顶点都满足 ,则G中存在哈密顿环.

推论:假设是一个阶简单图,如果G中任意一个顶点都满足,则G中存在哈密尔顿环.

相关证明可见:Hamilton通路和回路的充分条件 - imbiansl’s space (gitee.io)

参考

哈密顿路径问题- 维基百科,自由的百科全书 (wikipedia.org)

Hamilton通路和回路的充分条件 - imbiansl’s space (gitee.io)

【离散数学·图论】关于哈密顿图的判别条件总结_哈密顿图的判定方法_20xx阳喵的博客-CSDN博客

哈密尔顿道路与哈密尔顿回路- Rogn - 博客园 (cnblogs.com)

🔲 ☆

树状数组

转载至 骇客地球

树状数组的代码很简单, 但思想却很复杂

搞清楚每一句代码背后的细节, 私以为是很重要的

这篇文章从一个新的角度,讲述树状数组实际上在做什么

For the past few days, I have been reading various explanations ofthe Binary Indexed Tree. For some reason, none of the explanations weredoing it for me. All explanations told me the same thing over and overagain. I was not able to find the motive behind this data structure,intuition behind this data structure.

Finally, I decided to sit down, check some examples, diagram themout, check stack overflow and understand it. I now understand the beautyof this data structure, and I think, I can explain it. For those whohave gone through this and also for those who don't want to go throughthis phase, I am writing this post..

Let me tell you one thing, this is going to be a longer post. I willtry to cover all the things associated with it. I have included examplesfor understanding. Give it half an hour, you will surely get new thingto learn.

Wasting no time, lets have a well defined problem.

1
2
3
4
We will be given an array. And we are asked to answer few queries. 
Queries will be of two types:-
1) Update X Y : Increment value at Xth index by Y.
2) Sum L R : Print sum of values at index L to R inclusive.

Lets have a look at other approaches in short, before going for BIT(Binary Indexed Tree), so that you will know the need of BIT.

  1. We can update any value in the array in singlestep. So, update operation will need timeO(1). Also, forsum operation, we can traverse the array and find sum.That operation will take time O(n)in worst case.
  2. One more thing we can do. At each index, we will store thecumulative frequency i.e. we will store sum of all elements before itincluding itself. We can construct this new array in . Lets say thisarray as CF[]. After that, All the sum operation willtake time since we will just subtract CF[L-1] from CF[R] to get theanswer for sum L R. But well, we will need to construct CF[] or at leastupdate CF[] every-time update operation is made. The worst case timerequired for this will be .

Since, the queries are huge in number, we can not always afford timecomplexity O(n)too. So, here comes the BIT for ourrescue.


BINARY INDEXEDTREE or FENWICK TREE

CONSTRUCTION of BIT:

Lets have an example with us. Input array is:

1
2
[ 5 ] [ 1 ] [ 6 ] [ 4 ] [ 2 ] [ 3 ] [ 3 ]
1 2 3 4 5 6 7

Now, think of what we did in 2nd approach. For each index, we werestoring sum of all elements before that element to that index. Right?But because of that, we were needed to change values at all locationsfor every update.

Now think it this way, what if we store sum of some elements at eachindex? i.e. Each index will store sum of some elements the number mayvary. Similarly, for update operation also, we will need to update onlyfew values, not all. We will see how!

Formally, we will create some benchmarks. Each benchmark will storesum of all elements before that element; but other than thosebenchmarks, no other point will store sum of all elements, they willstore sum of few elements. Okay, if we can do this, what we will need todo to get sum at any point is - intelligently choosing right combinationof positions so as to get sum of all elements before that point. Andthen we will extend it to sum of elements from L to R (for this, theapproach will be same as we did in second approach). We will see thatafterwards.

Now, having done the base work, lets move ahead.

Before telling HOW will we be doing, I would like to tell you WHATare we going to do. To remind you, we are going to create BIT[] of giveninput array.

WHAT:

This is a kind of manual process I am showing.

The benchmarks I was talking about are the powers of 2. Each index,if it is a power of 2, will store the sum of all elements before that.And we will apply this repetitively so as to get what each index willstore.

Suppose, we have an array of 16 elements, [1 .. 16].

Powers of 2 are:- 1, 2, 4, 8, 16

These index will store sum of all elements before them.

Fine?

What about others?

Divide this array in two halves:- we get [1..8] and [9 .. 16].

Now think recursively what we did for array of 16, apply same forthis, okay?

Seems like little bouncer? Wait, have an example of 8 elements only.Say 8 elements are :

1
1   2   3   4   5   6   7   8

Ok, powers of 2 are: 1 2 4 8 so, in BIT[] indiced 1 2 4 8 will store1 = 1, 1 + 2 =3, 1 + 2 + .. + 4 = 10 and 1 + 2 + .. + 8 = 36respectively. Right? Remember, sum of all elements before that element?Right? Good. So, till now, BIT looks like this:-

1
2
[ 1 ] [ 3 ] [  ] [ 10 ] [   ] [   ] [  ] [36] 
1 2 3 4 5 6 7 8

Now, divide the given array in 2 halves.

Arr1:

1
1   2   3   4

Arr2:

1
5   6   7   8

Consider Arr1 first. Powers of 2 are:- 1 2 4 They already have theirright values, no need to update.

Now, Consider Arr2: Powers of 2 are: 1 2 4

So, at indices 1, 2 and 4 will store 5 = 5, 5 + 6 = 11, 5 + 6 + 7 + 8= 26 respectively.

These are the indices according to this new 4-element array. So,actual indices with respect to original array are 4+1, 4+2, 4+4 i.e. 5,6, 8. We will not care about index 8 as it is already filled in BIT[].Hence we will update position 5 and 6 now.

BIT will start looking like this now:-

1
2
[ 1 ] [ 3 ] [  ] [ 10 ] [ 5 ] [ 11 ] [  ] [ 36 ] 
1 2 3 4 5 6 7 8

I think you guys have got what we are doing. Applying same procedureon Arr1 and Arr2, we will get 4 - two element arrays (2 from Arr1 and 2from Arr2). Follow the same procedure, don't change the value at anyindex if it is already filled, you get this BIT finally.

1
2
[ 1 ] [ 3 ] [ 3 ] [ 10 ] [ 5 ] [ 11 ] [ 7 ] [ 36 ] 
1 2 3 4 5 6 7 8

Guys, do take an example of 16 element array and convert it to BITmanually to get the gist.

Now see how will we do this in program.

HOW:

We will continue with our previous example.

Now, start thinking of our array as a binary tree, like this:-

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BEFORE:
[ 5 ] [ 1 ] [ 6 ] [ 4 ] [ 2 ] [ 3 ] [ 3 ]
1 2 3 4 5 6 7

NOW:

4
[ 4 ]
/ \
2 6
[ 1 ] [ 3 ]
/ \ / \
1 3 5 7
[5] [6] [2] [3]

Now, we will change value at each node by adding the sum of nodes inits left sub-tree.

1
2
3
4
5
6
7
8
9
10
UPDATED VERSION:

4
[ 16 ]
/ \
2 6
[ 6 ] [ 5 ]
/ \ / \
1 3 5 7
[5] [6] [2] [3]

I think you have got what we have just done! Take each node, find sumof all nodes in its left sub-tree and add it to value of that node. Andthis is what we call is BIT.

1
2
3
BIT:
[ 5 ] [ 6 ] [ 6 ] [ 16 ] [ 2 ] [ 5 ] [ 3 ]
1 2 3 4 5 6 7

SUM and UPDATE operations:

Now, we have got the BIT. Lets move ahead and solve our realproblem.

Having this tree structure with us, it is to find sum of elementstill any index. The idea is to keep a variable ansinitialized to 0. Follow the path from root to the nodeindex. Whenever we need to follow a right link, add thevalue of current node to ans . Once we reach the node, addthat value too.

For example, If we want sum of elements upto index 3.

See again,

1
2
3
INPUT ARRAY is:
[ 5 ] [ 1 ] [ 6 ] [ 4 ] [ 2 ] [ 3 ] [ 3 ]
1 2 3 4 5 6 7

(so answer should come out as 5 + 1 + 6 = 12)

1
2
3
4
5
6
7
8
9
10
BIT is:

4
[ 16 ]
/ \
2 6
[ 6 ] [ 5 ]
/ \ / \
1 3 5 7
[5] [6] [2] [3]

Following the procedure given above.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1> node = root, ans = 0
2> node is 4, index is 3.
3> index < node, go left
4> node = 2, index is 3.
5> index > node, add value(node) to ans and go right
i.e. ans = ans + value(node 2)
i.e. ans = 0 + 6
i.e. ans = 6
Now, go right
6> node = 3, index = 3
7> node == index, add value of node 3 to ans and return ans
i.e. ans = ans + 6
i.e. ans = 12
return 12

In actual implementation, we will be following the reverse path i.e.from node to root.

We will go in actual implementation too. Just have look at updateoperation as well.

If we want to increment k the value atindex by say .

The idea is very similar to sum operation.

Follow the path from root to the node. Whenever we need to follow aleft link, add the value of to current node. Once we reach the node, addto that node too. This is because we will need to update the set ofnodes in the tree that include that node in its left subtree, so that itwill be consistent with our sum operation,right?index``k``k

I don't think there is any need of example for this case again.


Moving ahead to the implementation.

For this, we will play a bit with BITS -- BinaryNumbers. Here comes the fun with bits -- Binary numbers. Youwill have to do little more work here to figure out the things. I willtry my best though.

1
2
3
4
5
6
7
8
9
TREE is:
100
[ 16 ]
/ \
010 110
[ 6 ] [ 5 ]
/ \ / \
001 011 101 111
[5] [6] [2] [3]

We have just changed the representation of out indices to binary.Ok?

Now, For each index, find the right most SET-bit i.e. '1' and dropthe all zeros along with that '1'. We get,

1
2
3
4
5
6
7
8
     (--)
[ 16 ]
/ \
0 1
[ 6 ] [ 5 ]
/ \ / \
00 01 10 11
[5] [6] [2] [3]

Here is the thing to be observed. If we treat 0 as LEFT and 1 asRIGHT, each node tells you the path to be followed from root to reachthat node. Really? Have example, say node 5, which has 10 there, i.e.RIGHT and LEFT. This is the path we need to follow from root to 5. Coolthing, right?

The reason why this is important to us is, our sumand update operations depends on the this path. Arethey not? You remember, Left link, right link, right? During asum, we just care about the left links we follow.During an update, we just care about the right links wefollow. This binary indexed tree does all of this super efficiently byjust using the bits in the index.

The key thing behind the efficiency of BIT is:

Given any index n, the next node on the path from root to that indexwhere we go right is directly calculated by RESETing i.e. '0' the last(right most) SET-bit from the binary representation of . Apply thisuntil we reach the root.index

Lets have examples:

1
2
3
4
5
6
7
8
9
TREE is:
4
[ 16 ]
/ \
2 6
[ 6 ] [ 5 ]
/ \ / \
1 3 5 7
[5] [6] [2] [3]

Say index is 5. The path from 4 to 5 is [ 4 -> RIGHT -> 6 ->LEFT -> 5 ] i.e. we take RIGHT at 4. Binary Representation of 5 is101. RESET right-most SET-bit. 101 -> 100 4 is the one node fromwhere we will go right STOP here. We have reached the root.

Say index is 7. The path from 4 to 7 is [ 4 -> RIGHT -> 6 ->RIGHT -> 7 ] i.e. we take RIGHT at 4 and 6. Binary Representation of7 is 111. RESET right-most SET-bit. 111 -> 110 6 is the node fromwhere we will go right RESET right-most SET-bit. 110 -> 100 4 is thenode from where we will go right STOP here. We have reached theroot.

We will use this information in our implementation.

Implementation:

Now we know, how to go from any index to the root and find what allright-links come in our path.

I will repeat some part of what we have looked.

For SUM: The idea is to keep a variable initialized to 0. Follow thepath from root to the node. Whenever we need to follow a right link, addthe value of current node to . Once we reach the node, add that valuetoo.ans``index``ans

For UPDATE: Follow the path from root to the node. Whenever we needto follow a left link, add the value of to current node. Once we reachthe node, add to that node too.index``k``k

Now you have got the complete picture I guess. Everything of What wesaw, Why we saw?

For SUM, We need to follow RIGHT-links no matter from root to indexor reverse. And we also know how to do that. Right?

So algorithm is:

1
2
3
4
5
6
SUM(index):
ans = 0
while(index != 0):
ans += BIT[index]
index = Reset_Rightmost_SET_bit(index)
return ans

Now, the thing remain unanswered is: How to Reset rightmost SETbit?This is a very simple task which I have already covered in my this note.By some observations, we can arrive at a conclusion that, whenever wesubtract one from any number say n, the part before a right-most set bitremain same while part after right-most set bit gets inverted. So, justANDing these can solve our problem.

1
2
Reset_Rightmost_SET_bit(n):
return n&(n-1)

Please be focused and try to understand this.

We need all left links but we can only know right links with thetechnique we studied earlier.

We know that, dropping the right-most SET bit and part after thatgives us the path from root to node.

So, zeros which come after the right-most one are not useful to us atall.

We will use both these fact and try to find a way.

You must have observed, what happens when we add a 1 to right-mostSET bit of a number? [Consider scan from right to left]

  1. The first zero from right (which will come after i.e. left to,right-most ONE of number) turns into one
  2. Part after (i.e. left to) that ZERO remain unchanged and Part beforethat get inverted.

Is this not the exact reverse procedure of what used to happen in SUMoperation.

This is all what we wanted!

And this is the value i.e. index from which we needed to take theleft link to reach to our node from root.

We have successfully found the left link too.

Adding one to right most one is nothing but adding place value ofright-most ONE to the number.

Hence our Update operation is as simple as:

1
2
3
4
UPDATE(index, addition):
while(index < length_of_array):
BIT[index] += addition
index = index + (index & -index)

Try to check the similarity and difference, and you will never forgetagain.

Here I will stop. I guess you have everything what you need to knowabout Binary Indexed Tree as a data structure. Now I advice you toimplement it yourself and see if you can do it.

You can always refer to the code which I am providing you.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# About using the 2 functions:-
# For update, pass index of location to be updated, input array, BIT, value to be added to original number
# i.e. new value - original value
# For getting sum of elements in range l to r,
# Getsum returns sum of elements from beginning to index
# Pass index, input array & BIT to function
# getsum of l to r = getsum of r - getsum of (l-1)

def update(index, a, tree, value):
# index is index to be updated, a is input array / list, tree is BIT array, value is value to be added to original
# number at index location
add = value
n = len(a)
while index<n:
tree[index] += add
index = index + (index & (-index))

def getsum(index, a, tree):
# index is location upto which you want the sum of elements from beginning
# tree is BIT[], a is input array / list
n = len(a)
ans = 0
while(index>0):
ans += tree[index]
index = index - (index & (-index))
return ans

#Get the user input
n = int(raw_input("Number of Elements in array: "))
inputArray = list(map(int, raw_input("Elements in array: ").split()))
inputArray.insert(0,0) # insert dummy node to have 1-based indexing

#Initialise Binary Indexed Tree to 0's considering that input array is all 0's
BIT = []
for i in range(0, n):
BIT.append(0)

# Now we will construct actual BIT
# The 4th parameter is always an additional value which is to be added to element at index location
# since we have considered input array as 0 earlier (while initialising BIT), for updating, we will pass actual
# value
for i in range(1, n):
update(i, inputArray, BIT, inputArray[i])

If you like this, Let me know :) Like, Share, Upvote[at thetop]!!

Thank you for reading and also thanks for your patience.

🔲 ☆

乘法逆元

定义

如果整数a, x满足, 则将x称为的模逆元, 记作.

模逆元也叫模倒数, 可以理解为模p同余式中a的倒数即, 也就是说, 模p的条件下,是等价的。相似的有一些简单的性质如.

注意, a在模p条件下存在逆元的充分必要条件是, a与p互素。

意义

当我们要求,且b数值过大无法直接存储在变量中与a运算, 这时就可以使用乘法逆元。

由乘法逆元定义有

求法

扩展欧几里得算法

过程

已知, 扩展欧几里得算法可用于求的一组可行解, 而互质时,

代码实现

python

1
2
3
4
5
6
def exgcd(a, b):
if b == 0:
return 1, 0
x, y = exgcd(b, a % b)
return y, x - y*(a // b)
ans = (exgcd(a, p)[0] % p + p) % p # 求a关于p的逆元

费马小定理

费马小定理可用于在p为素数时互质的情况下求的逆元。

过程

由费马小定理, 当p为素数且a、p互质时, ,而a和a的逆元x满足, 即.

所以在满足上述条件时, 的逆元即为, 使用快速幂计算即可。

python

代码实现

1
2
3
4
5
6
7
8
9
def quick_pow(a, n, p):
ans = 1
while n:
if n & 1:
ans = ans * a % p
a = a * a % p
n >>= 1
return ans
ans = quick_pow(a, p - 2, p) # 求a关于p的逆元

线性求逆元(递推)

递推法用于求[1, a]区间的每个数mod p的逆元。

过程

 

 

 

 

所以有递推式;而对于始项1,事实上, 对于任意整数p, 都有 .

代码实现

c++

1
2
3
4
inv[1] = 1;
for (int i = 2; i <= n; ++i) {
inv[i] = (long long)(p - p / i) * inv[p % i] % p;
}

参考

乘法逆元的几种计算方法 |Menci's OI Blog

乘法逆元 -OI Wiki (oi-wiki.org)

乘法逆元详解 -MJT12044 - 博客园 (cnblogs.com)

🔲 ☆

欧几里得算法和扩展欧几里得算法

欧几里得算法

数学中,辗转相除法,又称欧几里得算法(英语:Euclideanalgorithm),是求最大公约数算法。辗转相除法首次出现于欧几里得的《几何原本》(第VII卷,命题i和ii)中,而在中国则可以追溯至东汉出现的《九章算术》。

过程

欧几里得算法基于一个非常简单的原理:对于两个数a和b(a > b),a和b的最大公约数与b和a - b的最大公约数相同。

重复的迭代这个过程, 使. 如此一来, 参数不断减小,最后某时刻两个参数的值必然相等, 此时a、b的值即为最大公约数.

减运算代码实现

1
2
3
4
5
6
def gcd(a, b):
if b == a: # 或 if b == 0, 因为b == a时再迭代一次后必然是gcd(a, 0)
return a
if a < b:
return gcd(b, a)
return gcd(a - b, b)
欧几里得算法过程

模运算

但一般情况下, 我们会使用模运算来减少迭代的次数。

设a(a > b)设为a = kb + c, c < b,则用减法的欧几里得迭代过程的前面一部分显然是 上述过程可以简化为 由此我们可以写出用模运算代替减法运算的代码

模运算代码实现

python

1
2
def gcd(a, b):
return a if b == 0 else gcd(b, a % b)

c++

1
2
3
int gcd(int a, int b) {
return b ? gcd(b, a % b) : a;
}
一个比较巧妙的点是, 如果a <b, 则, 通过一次递归调整回了第一个参数较大的情况。

模运算的迭代过程相比减运算是跳跃式的, 所以不一定会经过a==b这个状态,因此应该以b=0为结束条件。

欧几里得算法的时间复杂度为, 因为对于a、b(a > b), a %=b至少会让a减少一半以上。

扩展欧几里得算法

裴属定理

裴属定理:对于任意整数a、b,都能找到两个整数x、y使得ax + by = gcd(a, b).

设a、b的最大公约数为c, 则有a = i * c, b = j * c,且i、j互质。所以裴属定理的另一种形式是:对于两个互质的整数a、b,都能找到两个整数x、y使得ax + by = 1

过程

扩展欧几里得算法常用于寻找裴属定理的一组可行解。

, 就是我们要求的解。

在欧几里得算法中, 如果要求, 会递归的求.

.

化简得, 所以.

要求,只需先递归的求出即可。

在欧几里得算法的递归终点中, 要使, 一组可行的解是。到达终点后, 不断回溯对进行递推, 最后即可得到关于的一组可行解。

代码实现

python

1
2
3
4
5
def exgcd(a, b):
if b == 0:
return 1, 0
x, y = exgcd(b, a % b)
return y, x - y*(a // b)

c++

1
2
3
4
5
6
7
8
9
void exgcd(int a, int b, int &x, int &y) {
if (b == 0) {
x = 1, y = 0;
} else {
exgcd(b, a % b, x, y);
int t = x;
x = y, y = t - y * (a / b);
}
}

参考

辗转相除法 -维基百科,自由的百科全书 (wikipedia.org)

小知识:什么是「欧几里得算法」_吴师兄学算法(cxyxiaowu.com)

最大公约数 - OIWiki (oi-wiki.org)

裴蜀定理_百度百科(baidu.com)

扩展欧几里得算法 -维基百科,自由的百科全书 (wikipedia.org)

❌