今天碰到的一个看似简单的算法问题卡的我头疼:余额和构成余额的交易的问题,问 GPT 也没结果

196 天前
 nzynzynzy

我抽象一下这个问题:invoices 是销售行为,transaction 是对余额的操作行为,一通存取反复拉扯之后形成的余额,尝试去扣款,如果余额足够就允许交易,如果余额不足就拒绝交易。

const invoices = [
  { id: 'INV001', type: 'Buy', amount: 100 }
];

const balanceTransactions = [
  { id: 'JNL001', type: 'transaction', amount: 100 },
  { id: 'JNL002', type: 'transaction', amount: -100 },
  { id: 'JNL003', type: 'transaction', amount: 100 },
  { id: 'JNL004', type: 'transaction', amount: -100 },
  { id: 'JNL005', type: 'transaction', amount: 130}
];

以上情况中,应该是 JNL001 到 JNL005 都被使用了,截至 JNL005 余额还剩 30 块,invoice 是 100 块。换句话说,就是尽可能多地查找余额的操作行为,确定哪些行为可以归结到这张 invoice 下。

请问这叫什么算法,以下是 GPT 的结果肯定是不对的

function performVerification() {
  const verifications = []; // 存储核销结果

  let remainingAmount = 0; // 剩余待核销金额

  // 按日期排序单据数组
  const sortedInvoices = invoices.sort((a, b) => a.date - b.date);
  const sortedJournals = journals.sort((a, b) => a.date - b.date);

  // 遍历单据数组进行核销
  for (const invoice of sortedInvoices) {
    let verifiedAmount = 0; // 当前单据已核销金额
    const verifiedJournals = []; // 当前单据核销的 journal 数组

    // 倒序遍历 journals 进行核销
    for (let i = sortedJournals.length - 1; i >= 0; i--) {
      const journal = sortedJournals[i];

      if (journal.amount < 0 && Math.abs(journal.amount) <= remainingAmount) {
        // 负数调整余额
        verifiedAmount += Math.abs(journal.amount);
        verifiedJournals.push({ id: journal.id, amount: Math.abs(journal.amount) });
        remainingAmount -= Math.abs(journal.amount);
        sortedJournals.splice(i, 1); // 从数组中移除已核销的 journal
      }

      // 如果当前单据已核销金额达到单据金额,则退出循环
      if (verifiedAmount >= invoice.amount) {
        break;
      }
    }

    // 添加核销结果到数组
    verifications.push({ invoice, verifiedAmount, verifiedJournals });

    // 如果已核销所有 journals ,则跳出循环
    if (sortedJournals.length === 0) {
      break;
    }
  }

  return verifications;
}
2941 次点击
所在节点    程序员
34 条回复
ivvei
196 天前
问题描述得乱七八槽。transaction 只能叫发生,不能叫使用。而且你这 JNL005 之后不是还有 130 吗,怎么就 100 了?要关联 invoice 为何不直接在 transaction 里面带上 invoice 的编号?正常人都这么做的啊。你根据金额倒推,遇到同样金额的 invoice 不是懵逼了?
nzynzynzy
196 天前
@ivvei #1 叫发生没什么问题。这个是我抽象的结果,就是打个比方。用户希望通过一笔消费能看到这笔能对应上几笔发生。

这个 transaction 和 invoice 不是一一对应的,你可以想象为客户既可以消费也可以存取,存取并不是指定消费的。

我疏漏了一些描述:
- 就是一顿存取之后,只有一笔 invoice ,不会是多笔。
- 没用到的单子就会自动结转等待下一次使用。

这个数据,算法希望对应的结果应该是

JNL001 ,金额 100 ,使用 100 ,余额 0
JNL002 ,金额-100 ,使用-100 ,余额 0
JNL003 ,金额 100 ,使用 100 ,余额 0
JNL004 ,金额-100 ,使用-100 ,余额 0
JNL005 ,金额 130 ,使用 100 ,余额 30
zizon
196 天前
看着像币圈侧链/次级账户交易合并回主链的操作...

你合并 transaction 的时候检查更新余额看有没超不就行了?
NoOneNoBody
196 天前
我理解就叫结算啊,或者集中结算
JNL*是可实施或不实施?只是有序订单?赊账?

你这问题问得不清楚,太难明了,不怪 GPT 答错
RecursiveG
196 天前
建议楼主先去把自己的思路捋顺了再问。
Chad0000
196 天前
你把 transaction 理解为记账就理解了。transaction 加起来就是余额,余额你可以缓存/冗余起来。
rekulas
196 天前
有点像数字货币的 utxo 的概念

不过这个逻辑很简单,不清楚你有什么疑惑
wingor2015
196 天前
额, 看完之后感觉没完全看懂,建议楼主详细描述一下,比如举个例子账号从初始状态开始开始,然后每执行一次行为,invoices 和 transaction 各为什么状态,
nzynzynzy
196 天前
感谢各位耐心阅读。

比如以下事件按顺序发生

刚开始余额是 0
==============
操作余额#JNL001 ,金额 100
操作余额#JNL002 ,金额-100
操作余额#JNL003 ,金额 100
操作余额#JNL004 ,金额-100
操作余额#JNL005 ,金额 130
===============
截至目前账户余额是 130 元,此时发生了一单交易 INV001 ,价值 100 元
这一单需说明“这 100 块是从哪些余额操作中来的”,这时候目前面临无数种算法列举 3 种可能性

可能性 A:先进先出,够用就停
INV001 的 100 元来自 JNL001

可能性 B:先进先出,尽可能地记录
INV001 的 100 元来自 JNL001-JNL005 的叠加
JNL001 ,金额 100 ,核销 100 ,余额 0
JNL002 ,金额-100 ,核销-100 ,余额 0
JNL003 ,金额 100 ,核销 100 ,余额 0
JNL004 ,金额-100 ,核销-100 ,余额 0
JNL005 ,金额 130 ,核销 100 ,余额 30

可能性 C:
选一个够用的最大的,即 JNL005 去核销,其他无视

这个例子中追求的算法是以上的 B ,即尽可能记录 INV001 的来源
nzynzynzy
196 天前
确实这个东西昨天让我有点心烦意乱的,我打了个草稿放在 V2 然后今天发了,可能真的懂得只有我。

=============================
然后还有一个规则就是“贪婪”先进先出地消耗 JNL ,耗完就停止,举个例子

刚开始余额是 0
----
操作余额#JNL001 ,金额 100
操作余额#JNL002 ,金额-100
操作余额#JNL003 ,金额 100
操作余额#JNL004 ,金额-100
操作余额#JNL005 ,金额 130
销售 INV001 ,金额 100=>操作核销掉 JNL001-004 全部余额,因为他们叠加起来余额是 0 ,然后核销掉 JNL005 的 100 元,剩 30 元
-----
操作余额#JNL006 ,金额 100
这时候发生交易 INV002 ,金额 130 ,此时需要记录 INV002 的来源:
JNL005 ,金额 30 ,核销 30 ,余额 0 ,
JNL006 ,金额 100 ,核销 100 ,余额 0

情况大概就是这么个情况,看看还有什么没讲明白

=============================

@zizon #3
@NoOneNoBody #4
@RecursiveG #5
@Chad0000 #6
@rekulas #7
@wingor2015 #8

烦请各位如果有思路分享一下,有兴趣也可以插眼回来看
Sawyerhou
196 天前
我看了 15min 都没看明白,这是行情数据吗?

inv 代表 K 线的成交额
trans 代表每笔订单的成交额
每根 K 线成交额由数量不确定的订单成交额加总得到

现在想做数据对齐
找出指定 K 线由哪些订单合成的?

如果是想问以上的话
这个对齐几乎不可能
NessajCN
196 天前
我也完全没看懂,你说的是中文吗
JNL 是啥?为啥要纠结余额从哪里扣,
是不是你有跟这个楼主 https://v2ex.com/t/1033839 类似的需求,也就是余额会过期?
NoOneNoBody
196 天前
pandas 有个函数叫 cumsum ,就是对一个数列,每个元素,向首个元素方向所有包含的元素求和,得出一个新数列
例如 1,2,3,4,5 结果为 1,3,6,10,15

现有一个数 5 ,满足 x>=5 ,求 x 的位置
对应 A ,从首位开始,6 为第一个满足的,位置为 3 ,有效的就是前三条
对应 B ,从末尾开始,15 为第一个满足的,位置为 5 ,则全部有效
C 就是 1,2,3,4,5 中选一个,与 cumsum 无关,位置为 5 ,有效的为仅第五条
NoOneNoBody
196 天前
@all
OP 实际需要求的是一个条件 mask ,覆盖若干条 JNL 构成 True
如果上述 JNL 有序,可按#13 方案;如果无序,那就复杂了,需要排列组合
Chad0000
196 天前
@NessajCN
op 应该在做 invoice 结算系统。invoice 提前发给客户,然后客户在某个时间早期转账支付了就行。那个 transaction 就是支付记录。国外很多系统都是这么运行。


如果是这样,客户打款直接记录进账 transaction ,然后触发余额检查,如果够就产生一笔扣 invoice 的 transaction ,如果不够可以有多少扣多少。如果客户多转钱了,还是扣 invoice 那么多,但会导致所有 transaction 加起来为正数,也就是用户的账户处于 credit 状态(有余额)。

不知我怎么说 op 明白了没有?还是你的系统不是这样的?
@nzynzynzy
@nzynzynzy
@nzynzynzy
nzynzynzy
196 天前
@Chad0000 #15 原理有点类似,或者你可以理解为预存/预支 + 购买,而且这个购买的时候要找到对应的预存/预支记录,而且预存/预支是有可能像我描述的可能性 A 这样被第一个记录拦截住,而后面其实还有预支。理想情况是能读取到第五张单据

@NoOneNoBody #13 谢谢,cumsum 确实是这个问题目前最贴切的描述,就是贪婪版的 cumsum ,我去朝着这个方向再探一探

也欢迎其他朋友继续看看!
nzynzynzy
196 天前
@NessajCN #12 多谢这个还真的很类似!我感觉余额会过期是一种类似的情况,都是有序的先进先出地消耗,我继续去读一下
NessajCN
196 天前
@nzynzynzy 那你就也参考区块链只记录交易信息呗。
nzynzynzy
196 天前
@Chad0000 #15 而且你的例子中假设是 invoice 是可以 pending 状态先进来等待付款,和我的情况有差别,我的更像是进来时候实时判断钱够不够,不够这笔就废止了。我觉得老哥说的 cumsum 是有道理的,而且是正负都存在的 cumsum 。

这个就变成了:cumsum 的数组中找值大于 invoice 的情况,其中最靠队尾的、值大于等于 invoice 的一个或者一群值中最靠前的一个。累死我了这个描述。。。
ddzzhen
196 天前
既然有冲有送,应该把金额按来源分类打标签,金额与 transaction 独立,这样遇到 invoices 时候才容易明确来源和条件判断

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1039989

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX